Migrating 200+ Config Files from JSON to YAML Without Breaking Production
· Cosyslabs
A startup running a microservices platform (details anonymized) maintained over 200 JSON configuration files across 12 services. As their DevOps team grew and Kubernetes adoption deepened, they decided to migrate all service configuration to YAML for better readability, comment support, and consistency with their Kubernetes manifests. Here is how they completed the migration across three weeks without a single production incident.
Why They Migrated
Their JSON configuration files had grown unwieldy:
{
"server": {
"port": 8080,
"timeout": 30000,
"maxConnections": 100
},
"database": {
"host": "postgres-primary",
"port": 5432,
"name": "appdb",
"poolMin": 2,
"poolMax": 20,
"connectionTimeout": 5000
},
"features": {
"newCheckout": false,
"betaSearch": false,
"darkMode": true
}
}
Three problems:
- No comments: why was
timeout30,000 and not 60,000? Why wasnewCheckoutdisabled? No context anywhere. - Inconsistency with Kubernetes: their Helm charts were YAML. Engineers constantly switched mental models.
- Diff noise: JSON required commas after every value, causing meaningless diff lines when adding or removing fields.
The Migration Plan
The team created a three-phase plan:
Phase 1: Convert files to YAML, validate equivalence, commit to a feature branch
Phase 2: Run both JSON and YAML configs in staging for one week, verify application behavior
Phase 3: Switch production one service at a time, maintain JSON fallback for 30 days
Phase 1: Automated Conversion
They wrote a Node.js script to convert all JSON files:
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
function jsonToYaml(jsonPath) {
const content = fs.readFileSync(jsonPath, "utf8");
const parsed = JSON.parse(content);
// Convert to YAML with 2-space indent
const yamlContent = yaml.dump(parsed, {
indent: 2,
lineWidth: -1, // No line wrapping
noRefs: true, // No YAML anchors (keep it simple)
sortKeys: false, // Preserve insertion order
});
return yamlContent;
}
// Process all config files
const configDir = "./configs";
const files = fs.readdirSync(configDir).filter(f => f.endsWith(".json"));
for (const file of files) {
const jsonPath = path.join(configDir, file);
const yamlPath = jsonPath.replace(".json", ".yaml");
const yamlContent = jsonToYaml(jsonPath);
fs.writeFileSync(yamlPath, yamlContent);
console.log(`Converted: ${file} → ${file.replace(".json", ".yaml")}`);
}
Validating Equivalence
For each converted pair, they verified that round-tripping through parse → serialize produced identical data:
function validateConversion(jsonPath, yamlPath) {
const fromJson = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
const fromYaml = yaml.load(fs.readFileSync(yamlPath, "utf8"));
const jsonNormalized = JSON.stringify(fromJson, null, 2);
const yamlNormalized = JSON.stringify(fromYaml, null, 2);
if (jsonNormalized !== yamlNormalized) {
console.error(`MISMATCH: ${jsonPath}`);
console.error("JSON:", jsonNormalized.slice(0, 500));
console.error("YAML:", yamlNormalized.slice(0, 500));
return false;
}
return true;
}
197 of 200 files passed on the first run.
The YAML Pitfalls They Hit
Three files failed validation due to YAML's type inference rules.
Pitfall 1: The Norway Problem
One configuration file had country codes:
{
"supported_countries": ["US", "GB", "DE", "NO", "SE", "FR"]
}
The YAML output from js-yaml.dump() was:
supported_countries:
- US
- GB
- DE
- NO
- SE
- FR
When parsed back, NO became false (YAML 1.1 boolean). The round-trip validation caught this immediately. The fix: the js-yaml dump option styles: { '!!null': 'empty' } was insufficient; they needed explicit quoting:
const yamlContent = yaml.dump(parsed, {
quotingType: '"', // Use double quotes for strings that might be ambiguous
forceQuotes: false, // Don't quote everything, just ambiguous values
// Custom schema to avoid YAML 1.1 boolean keywords
schema: yaml.JSON_SCHEMA, // Uses JSON types only — no YAML 1.1 quirks
});
Switching to yaml.JSON_SCHEMA resolved all three failures. This schema tells js-yaml to serialize values in ways that parse back correctly under strict JSON-compatible rules.
Pitfall 2: File Permission Octal Values
A deploy configuration had file permissions:
{
"umask": 22,
"log_dir_permissions": 493
}
Their application developer had stored these as decimal integers in JSON. In YAML, 022 would have been parsed as octal by a YAML 1.1 parser. Using the JSON schema avoided this, but the team added a comment explaining the values:
# umask 022 in decimal — stored as decimal to avoid YAML octal ambiguity
umask: 22
# chmod 755 in decimal
log_dir_permissions: 493
Pitfall 3: Timestamps
A monitoring config contained an ISO date:
{
"last_reset": "2026-01-01"
}
YAML parsers that support timestamps (YAML 1.1 and some YAML 1.2 parsers) automatically convert 2026-01-01 to a Date object. When round-tripped through JSON, it became "2026-01-01T00:00:00.000Z" — different from the original string. The fix: quote all date-like strings, or use yaml.JSON_SCHEMA which treats them as strings.
Phase 2: Staging Validation
During staging, they ran both configs in parallel:
// Config loader — supports both formats
async function loadConfig(serviceName) {
const yamlPath = `./configs/${serviceName}.yaml`;
const jsonPath = `./configs/${serviceName}.json`;
if (fs.existsSync(yamlPath)) {
const content = fs.readFileSync(yamlPath, "utf8");
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
}
return JSON.parse(fs.readFileSync(jsonPath, "utf8"));
}
Staging ran for one week. No behavioral differences were detected — the validation script had caught all data discrepancies.
Adding Comments (The Main Benefit)
After migration, engineers added context that would never have been possible in JSON:
server:
port: 8080
# Increased from 10s to 30s in Q1 2026 due to slow PDF generation
# See issue #847 — revert if PDF service is replaced
timeout: 30000
# Based on load tests showing diminishing returns above 100 concurrent
maxConnections: 100
features:
# Disabled pending A/B test completion (est. Q3 2026)
# Owner: @product-team, ticket: FEAT-2041
newCheckout: false
# Beta since 2025-09 — graduation target Q2 2026
betaSearch: false
Phase 3: Production Rollout
One service per day over 12 days. The deployment checklist for each service:
- Verify YAML config passes validation script
- Deploy with
CONFIG_FORMAT=yamlenvironment variable - Monitor error rates for 30 minutes
- If stable, proceed; if any errors, revert to JSON with
CONFIG_FORMAT=json
Zero rollbacks were required.
Tooling Used
Throughout the migration, the team used two browser tools extensively:
- JSON Formatter: validating and prettifying JSON files during initial review
- YAML Formatter: spotting indentation issues and validating YAML syntax during manual review of converted files
Both tools provided instant parse error feedback without requiring them to run a Node.js script for every check.
Summary
The migration succeeded because of:
- Automated conversion with round-trip validation — no manual copying
- Using
yaml.JSON_SCHEMA— avoids all YAML 1.1 type inference quirks - Staging dual-mode period — confirmed no behavioral differences before production
- Per-service rollout — contained blast radius to one service if something went wrong
Total time: three weeks for 12 services. Production incidents: zero.