mat conversion at runtime. Instead, establish explicit boundaries:
- APIs and internal services consume JSON exclusively.
- Configuration repositories and orchestration tools author YAML exclusively.
- A build-time or deployment-time converter bridges the two, validated against a shared schema.
Step 3: Integrate Schema Validation
Both formats require contract enforcement. JSON integrates natively with JSON Schema. YAML requires conversion to an intermediate representation before validation, or direct schema checking via language-specific libraries. Validation must occur before application boot to fail fast on malformed configuration.
Architecture Decision: TypeScript Configuration Loader
The following implementation demonstrates a production-grade configuration module that enforces format boundaries, applies strict validation, and prevents implicit type coercion.
import { readFileSync } from 'fs';
import { parse as parseYaml } from 'yaml';
import { z } from 'zod';
// Shared contract for both formats
const AppConfigSchema = z.object({
server: z.object({
host: z.string().default('0.0.0.0'),
port: z.number().int().positive().default(3000),
enableDebug: z.boolean().default(false),
}),
database: z.object({
connectionUri: z.string().url(),
maxConnections: z.number().int().min(1).max(50).default(10),
}),
});
type AppConfig = z.infer<typeof AppConfigSchema>;
class ConfigLoader {
private readonly schema: z.ZodType<AppConfig>;
constructor() {
this.schema = AppConfigSchema;
}
loadFromJson(filePath: string): AppConfig {
const raw = readFileSync(filePath, 'utf-8');
const parsed = JSON.parse(raw);
return this.schema.parse(parsed);
}
loadFromYaml(filePath: string): AppConfig {
const raw = readFileSync(filePath, 'utf-8');
// YAML 1.2 strict mode prevents implicit boolean/number coercion
const parsed = parseYaml(raw, { version: '1.2', strict: true });
return this.schema.parse(parsed);
}
validateAndNormalize(rawData: Record<string, unknown>): AppConfig {
return this.schema.parse(rawData);
}
}
export const configLoader = new ConfigLoader();
Why this architecture works:
- Shared Zod schema guarantees identical validation rules regardless of source format.
- Explicit YAML 1.2 strict mode disables legacy boolean coercion (
yes/no/on/off), eliminating the "Norway problem" where country codes or feature flags resolve incorrectly.
- Fail-fast parsing throws at load time rather than during runtime execution, preventing partial initialization states.
- Type inference ensures TypeScript catches mismatches during compilation, reducing defensive coding in business logic.
When conversion is unavoidable (e.g., Kubernetes manifests submitted to an API expecting JSON), use a deterministic transformer that strips YAML-specific features before serialization.
import { stringify as stringifyYaml } from 'yaml';
function convertYamlToJson(yamlContent: string): string {
const parsed = parseYaml(yamlContent, { version: '1.2', strict: true });
return JSON.stringify(parsed, null, 2);
}
function convertJsonToYaml(jsonContent: string): string {
const parsed = JSON.parse(jsonContent);
return stringifyYaml(parsed, {
indent: 2,
noRefs: true, // Disables anchors/aliases for portability
lineWidth: -1 // Prevents automatic line wrapping
});
}
Rationale: Disabling YAML references (noRefs: true) prevents circular dependency bugs when files are split across repositories. Setting lineWidth: -1 preserves multi-line strings exactly as authored, avoiding unintended folding that breaks shell scripts or SQL queries embedded in configuration.
Pitfall Guide
1. Implicit Type Coercion
Explanation: YAML 1.1 automatically converts true, false, yes, no, on, off, and numeric-looking strings into booleans or numbers. A configuration value like feature_flag: off becomes a boolean false, breaking string comparisons downstream.
Fix: Enforce YAML 1.2 strict mode in all parsers. Wrap scalar values in quotes when string type is required. Validate against a schema that explicitly declares expected types.
2. Indentation Fragility
Explanation: YAML uses whitespace to define hierarchy. A single misplaced space or mixed tab/space indentation silently restructures the document or triggers parser exceptions with unhelpful error messages.
Fix: Implement .editorconfig with indent_style = space and indent_size = 2. Add a pre-commit hook running yamllint with rules: indentation: { spaces: 2 }. Fail CI on any whitespace deviation.
3. Over-Engineering Configuration
Explanation: YAML supports complex nesting, anchors, and conditional-like structures. Teams often embed business logic, environment branching, or dynamic defaults directly into config files, creating unmaintainable artifacts.
Fix: Treat configuration as data, not code. Keep structures flat where possible. Move conditional logic, defaults, and environment-specific overrides into the application layer or use a dedicated configuration management tool (e.g., Consul, AWS AppConfig).
Explanation: Developers frequently pipe YAML output directly into JSON parsers or vice versa without validation. YAML comments, anchors, and implicit types break JSON parsers, while JSON's quoted keys and lack of comments trigger YAML parsing warnings.
Fix: Never perform runtime format conversion without schema validation. Use a dedicated transformation step that strips format-specific features before cross-format serialization.
5. Parser Version Fragmentation
Explanation: Different languages and libraries implement YAML 1.1, 1.2, or custom dialects. A manifest that parses correctly in Python's PyYAML may fail in Go's gopkg.in/yaml.v3 due to differing type resolution rules.
Fix: Pin parser versions in all services. Document the exact YAML spec version your stack supports. Test configuration files across all runtime environments during CI.
6. Missing Schema Validation
Explanation: Relying on runtime type checks or manual inspection leaves configuration errors undetected until deployment. Missing required fields or incorrect nested structures cause cascading failures.
Fix: Integrate JSON Schema or Zod validation at application startup. Reject invalid configuration immediately with clear error paths. Generate schema documentation automatically for team reference.
7. Tab/Space Inconsistency
Explanation: YAML explicitly forbids tabs for indentation. Editors configured with tab insertion or auto-formatting tools that convert spaces to tabs produce unparseable files.
Fix: Configure IDEs to convert tabs to spaces on save. Enforce insert_final_newline = true and trim_trailing_whitespace = true in .editorconfig. Add a CI step that scans for tab characters in all .yaml and .yml files.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| REST/GraphQL API payloads | JSON | Universal parser support, strict typing, JSON Schema compatibility | Low (standardized tooling) |
| Kubernetes/Helm manifests | YAML | Ecosystem standard, human-editable, supports multi-line scripts | Medium (requires linting & validation) |
| CI/CD workflow definitions | YAML | Native support in GitHub Actions, GitLab CI, CircleCI | Low (platform-enforced) |
| Internal service-to-service events | JSON | Deterministic parsing, schema validation, lower latency | Low |
| Environment overrides & secrets | JSON | Strict structure prevents accidental type coercion, easier to diff | Low |
| Database connection configs | YAML | Readable for ops teams, supports comments for documentation | Low |
Configuration Template
Copy this template into your project root to enforce format boundaries and validation from day one.
# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yaml]
indent_size = 2
[*.json]
indent_size = 2
// .yamllint.yml
extends: default
rules:
indentation:
spaces: 2
indent-sequences: true
line-length:
max: 120
allow-non-breakable-words: true
comments:
min-spaces-from-content: 1
truthy:
allowed-values: ['true', 'false', 'yes', 'no']
check-keys: false
// src/config/loader.ts (Zod + YAML/JSON enforcement)
import { z } from 'zod';
import { parse as parseYaml } from 'yaml';
import { readFileSync } from 'fs';
export const InfrastructureSchema = z.object({
cluster: z.string().min(3),
region: z.string().length(2),
replicas: z.number().int().min(1).max(100),
features: z.record(z.boolean()),
});
export function loadInfrastructureConfig(path: string, format: 'json' | 'yaml') {
const raw = readFileSync(path, 'utf-8');
const parsed = format === 'json'
? JSON.parse(raw)
: parseYaml(raw, { version: '1.2', strict: true });
return InfrastructureSchema.parse(parsed);
}
Quick Start Guide
- Initialize format boundaries: Create
src/config/ with separate api.json and infra.yaml files. Define a shared Zod schema that both files must satisfy.
- Add validation to startup: Import the schema and loader in your application entry point. Call
loadInfrastructureConfig() before initializing servers or database connections. Throw on validation failure.
- Enforce linting: Install
yaml and zod via npm. Add .editorconfig and .yamllint.yml to the repository root. Configure your CI pipeline to run yamllint on all YAML changes and tsc --noEmit to catch type mismatches.
- Test cross-format conversion: If your deployment pipeline requires JSON output from YAML manifests, implement the conversion utility shown in the Core Solution. Run it in a pre-deployment step with schema validation on both sides.
- Monitor parse health: Add structured logging around configuration loading. Track validation errors, format mismatches, and parser exceptions. Alert when error rates exceed 0.1% of deployments.
By treating format selection as an architectural constraint rather than a stylistic preference, you eliminate silent type coercion, reduce CI/CD flakiness, and establish a validation contract that scales across teams and services. JSON guarantees machine reliability; YAML optimizes human iteration. Align each with its intended lifecycle, enforce boundaries with schema validation, and your configuration layer will remain stable as system complexity grows.