Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Version Control Best Practices for JSON Configuration Files
JSON configuration files become hard to manage when every editor rewrites them differently, arrays get reordered for no semantic reason, and production-only values or secrets live next to safe defaults. Good version control practice is really about making each JSON change deterministic, reviewable, and easy to validate before it reaches your main branch.
The workflow that scales is simple: normalize formatting before commit, structure config so unrelated teams do not edit the same lines, validate shape and types automatically, and keep sensitive values out of Git. If you do those four things consistently, Git becomes much better at showing real intent instead of formatting noise.
What Usually Goes Wrong
- One logical change is mixed with a whole-file reformat, so reviewers cannot see what actually changed.
- Large arrays are used for items that really have stable IDs, which makes inserts and reordering conflict-prone.
- Base config, environment overrides, and developer-local values are all committed in the same file.
- Broken JSON or wrong value types are only discovered after deployment because validation is manual.
- Secrets or machine-specific values leak into history and are painful to rotate later.
The Short Answer
- Commit one config change at a time and keep formatting-only rewrites in their own commit.
- Use one canonical JSON style so diffs stay small and predictable.
- Prefer keyed objects over arrays when item order is not meaningful.
- Validate config with JSON Schema in CI before merge and before deploy.
- Store defaults in Git, store secrets elsewhere, and review config diffs as carefully as code.
Best Practices in Detail
1. Make Every Config Commit Atomic and Reversible
A good config commit answers one clear question: what behavior changed? Updating a timeout, enabling a feature flag, or introducing a new service endpoint should each be separate commits. If you also need to reformat the file, do that in a dedicated formatter-only commit so the semantic change stays obvious and easy to revert.
2. Canonicalize Formatting Before Every Commit
JSON diffs are only readable when every file is serialized the same way every time. Use the same indentation, line endings, final newline behavior, and property layout for the whole repo. Keep one property per line in objects that humans review often, and only sort keys if your application does not attach meaning to display order.
Most importantly, keep the file valid JSON. JSON does not allow comments, so do not put inline notes inside the config itself. If reviewers need context, put it in a schema, README, or adjacent documentation file.
Recommended serialized style
{
"api": {
"baseUrl": "https://api.example.com",
"timeoutMs": 5000
},
"features": {
"newCheckout": true,
"searchV2": false
}
}A JSON formatter is the easiest place to enforce this. Run it locally before commit and again in CI so the branch cannot drift into multiple serialization styles.
3. Prefer Keyed Objects Over Arrays When Order Does Not Matter
Arrays are fine when order is part of the meaning, such as middleware order or a priority list. They are a poor fit for collections of named items like feature flags, services, tenants, or per-market settings. Turning those collections into objects keyed by a stable identifier dramatically reduces insert and reorder conflicts.
Conflict-prone array
{
"services": [
{
"name": "billing",
"timeoutMs": 2000
},
{
"name": "search",
"timeoutMs": 1000
}
]
}Stable object keyed by ID
{
"services": {
"billing": {
"timeoutMs": 2000
},
"search": {
"timeoutMs": 1000
}
}
}This one change often does more for merge quality than any Git setting, because Git is working with smaller, more localized line edits.
4. Split Base Config, Environment Overrides, and Local Secrets
Teams run into trouble when every environment is encoded in one giant file. A cleaner pattern is to commit a safe base file, add small environment-specific override files for non-secret differences, and keep developer-local overrides or secrets out of version control entirely.
Typical repo layout
config/ base.json production.json staging.json config.local.json # gitignored
This keeps production changes narrow and lets local development stay flexible without polluting shared config history.
5. Validate With JSON Schema in CI
Formatting solves readability, not correctness. To catch missing keys, wrong types, and unexpected fields, validate your config in automation. The current JSON Schema specification is Draft 2020-12, and even a small schema gives reviewers a much stronger safety net than manual eyeballing.
Minimal schema for a config file
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"api": {
"type": "object",
"properties": {
"baseUrl": { "type": "string" },
"timeoutMs": { "type": "integer", "minimum": 0 }
},
"required": ["baseUrl", "timeoutMs"],
"additionalProperties": false
}
},
"required": ["api"],
"additionalProperties": false
}Run schema validation in pre-commit hooks if you want quick local feedback, but always enforce it again in CI so the main branch cannot accept invalid JSON.
6. Use Git Settings That Improve Review, Not Just Silence Conflicts
Git supports path-specific diff behavior through .gitattributes. For JSON files, a dedicated diff driver with a minimal diff algorithm can make reviews cleaner by shrinking noisy hunks around nearby changes.
Simple JSON diff setup
# .gitattributes *.json diff=json # .git/config or ~/.gitconfig [diff "json"] algorithm = minimal
That helps diff output, but it does not magically make merges safe. Avoid using merge=union for JSON just to get fewer conflict markers. For structured data, a silent but incorrect merge is worse than an explicit conflict.
Advanced: custom JSON-aware merge driver
# .gitattributes *.json merge=json # .git/config [merge "json"] name = json-aware merge driver = your-json-merge %O %A %B %L %P
Only do this if the merge driver actually parses JSON and re-serializes it deterministically. If it just concatenates text, you are trading visible conflicts for broken config.
7. If You Introduce Normalization Later, Roll It Out Deliberately
Adding a formatter, line-ending normalization, or a clean/smudge filter to an existing repo can create noisy three-way merges for a while. The safest rollout is a dedicated normalization commit, followed by regular semantic commits. During that transition, Git's merge.renormalize setting can reduce spurious conflicts caused by the serialization change itself.
8. Keep Secrets and Machine-Specific Values Out of Git
JSON config in version control should describe safe defaults and references, not live credentials. Use environment variables, a secrets manager, or deployment-time injection for sensitive values. If developers need a template, commit a sample that points to the secret name rather than the secret itself.
Commit a reference, not the secret
{
"database": {
"host": "db.internal",
"passwordEnv": "DB_PASSWORD"
}
}Secret scanning in CI is worth adding as a backstop, but the better practice is to keep the secret out of the tracked file in the first place.
9. Review Config Pull Requests Like Deployment Changes
When JSON controls runtime behavior, the review bar should look closer to an infrastructure change than a typo fix.
- Is the diff mostly semantic, or is it hiding inside a formatting rewrite?
- Does the new value have the right unit, range, and environment scope?
- Will the change behave safely for existing users or only for fresh deployments?
- Did schema validation, tests, and secret checks pass on the pull request?
- Is rollback obvious if the config change needs to be reversed quickly?
10. Document Generated JSON and Human Authoring Rules
Some JSON files are hand-edited. Others are generated from a higher-level source. Mixing those workflows without documentation creates churn. If a file is generated, document the source of truth and the exact regeneration command. If humans need comments or richer authoring features, consider maintaining JSONC, TOML, or YAML as source and emitting stable JSON as build output.
The important part is determinism: whether humans or tools edit the source, the committed JSON should be reproducible and serialized the same way every time.
When a Merge Conflict Happens Anyway
- Reformat both sides to the canonical style first so you are resolving intent, not whitespace.
- Check whether the conflict is really caused by an unnecessary array or mixed environment values.
- Resolve the JSON, then run schema validation and any config-related tests before committing.
- Write a commit message that explains the behavior you kept, not just that you fixed a conflict.
Bottom Line
The best version control strategy for JSON configuration files is to reduce ambiguity before Git ever has to help you. Canonical formatting, conflict-resistant structure, schema validation, and strict secret handling produce smaller diffs, safer reviews, and fewer merge surprises. Add Git-specific diff and merge tuning on top of that foundation, and JSON config stops feeling fragile.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool