Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Schema Versioning for JSON Configuration Files
Treat a JSON config format like an API. If a file can outlive one application release, be edited by humans, or be shared across services and tools, its structure will eventually change. Without explicit schema versioning, those changes become guesswork, and guesswork is what breaks loaders, deploys, and rollback paths.
The safest default for most teams is simple: put a `schemaVersion` field in every config, validate each version explicitly, and migrate older files into one latest in-memory representation before the rest of the application touches them.
Recommended default
- Use a root-level `schemaVersion` field for the document format.
- Keep the application version separate from the config schema version.
- Validate against a schema for that exact version before migration.
- Migrate step-by-step from older versions to one latest internal model.
- Deploy readers that understand the new version before writers start emitting it.
Why Config Schemas Break Over Time
A configuration that starts simple often grows new nesting, defaults, and validation rules.
Config v1
{
"schemaVersion": 1,
"featureEnabled": true,
"message": "Feature is active!"
}Config v2
{
"schemaVersion": 2,
"feature": {
"enabled": true,
"messages": {
"admin": "Admin feature message",
"user": "User feature message"
},
"startsAt": "2026-03-10T09:00:00Z"
}
}A v1 reader expects top-level `featureEnabled` and `message` fields. If it reads v2 without a version check, it can fail outright or, worse, silently misinterpret the data. Explicit versioning turns that failure into a controlled branch instead of an accident.
The Practical Baseline
- Version the document, not just the file name. File names and directories can help, but the version should travel with the JSON itself.
- Normalize to one internal shape. The rest of the codebase should consume one latest structure, not branch on every historical format.
- Keep migrations incremental. Prefer `v1 -> v2 -> v3` functions over a growing set of special-case conversions from every old version directly to the newest.
- Decide your support window. Be explicit about how many old schema versions the loader will still accept and when they become unsupported.
When To Bump The Version
Do not tie schema versioning to every application release. Bump the config schema version when compatibility changes, not just because you shipped new code.
- Removing, renaming, or moving a required field is a version bump.
- Changing a field type or meaning is a version bump.
- Tightening validation rules enough to reject old valid files is a version bump.
- Adding an optional field may not need a version bump if older readers safely ignore unknown properties and the new field does not change existing behavior.
- Changing defaults that materially change runtime behavior should be treated as a versioned change.
Integer versions such as `1`, `2`, and `3` are usually the simplest choice for configuration files. Use semantic versioning only when external users or tools need the extra signal that major means incompatible, minor means backward-compatible additions, and patch means non-structural fixes.
Do Not Confuse `schemaVersion`, App Version, And `$schema`
- `schemaVersion` is your config document format. Your loader uses it to choose validation and migration logic.
- Application version is your binary or release number. It should stay separate because one app version may read several config schema versions.
- `$schema` belongs in the JSON Schema file. It tells validators which JSON Schema dialect that schema file uses.
This distinction matters because `$schema` is not a replacement for `schemaVersion`. A config file can be validated by a JSON Schema written in the 2020-12 dialect while still being your own document version 2 or 7.
Validate Each Version With JSON Schema
If you use JSON Schema today, the published 2020-12 draft is the right default unless your validator or platform is pinned to an older draft. Keep one schema file per config version and make the version check part of the schema itself.
Example schema for config v2
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://offlinetools.org/schemas/app-config.v2.schema.json",
"title": "AppConfig v2",
"type": "object",
"properties": {
"schemaVersion": {
"const": 2
},
"feature": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"startsAt": { "type": "string", "format": "date-time" },
"messages": {
"type": "object",
"properties": {
"admin": { "type": "string" },
"user": { "type": "string" }
},
"required": ["admin", "user"],
"additionalProperties": false
}
},
"required": ["enabled", "messages", "startsAt"],
"additionalProperties": false
}
},
"required": ["schemaVersion", "feature"],
"additionalProperties": false
}A strict schema catches accidental drift early: misspelled keys, wrong types, and forgotten required fields. If you intentionally allow extension points, document them clearly instead of silently accepting arbitrary extra properties everywhere.
Migrate To One Latest Representation
A good loader has a predictable pipeline: parse JSON, detect `schemaVersion`, validate against that version, migrate one step at a time until the latest version, then hand the normalized object to the application.
Conceptual loader
type AnyConfig = Record<string, unknown>;
const validators: Record<number, (value: AnyConfig) => boolean> = {
1: validateConfigV1,
2: validateConfigV2,
3: validateConfigV3,
};
const migrations: Record<number, (value: AnyConfig) => AnyConfig> = {
1: migrateV1ToV2,
2: migrateV2ToV3,
};
function loadConfig(jsonText: string): LatestConfig {
const raw = JSON.parse(jsonText) as AnyConfig;
const detectedVersion =
typeof raw.schemaVersion === "number" ? raw.schemaVersion : 1;
const validate = validators[detectedVersion];
if (!validate) {
throw new Error("Unsupported schemaVersion: " + detectedVersion);
}
if (!validate(raw)) {
throw new Error("Config failed validation for schemaVersion " + detectedVersion);
}
let current = raw;
for (let version = detectedVersion; migrations[version]; version += 1) {
current = migrations[version](current);
}
return parseLatestConfig(current);
}Runtime migration helps the newest code read older configs. Offline migration still matters for persistent files because it lets you rewrite them once, review the diff, and stop carrying historical formats forever.
Rollout Rules That Prevent Breakage
- Deploy readers before writers. The safe order is: release code that can read the new schema, then start writing the new schema.
- Never change meaning without changing version. Reusing the same version number for a different interpretation creates the hardest bugs to diagnose.
- Log deprecation warnings early. If schema version 1 will be removed, say so long before the removal release.
- Test round trips. Parse, migrate, write, and re-read representative real configs instead of only synthetic fixtures.
- Keep human editing in mind. Good error messages matter because many config failures are caused by manual edits, not code generation.
Large configuration-driven systems follow this same pattern. Kubernetes, for example, puts an explicit `apiVersion` in manifests because configuration often survives longer than any single binary release.
Common Mistakes
- Relying only on file names such as `config.v2.json` and leaving the JSON itself ambiguous.
- Using `version` without documenting whether it means app release, business rule set, or schema format.
- Copying an outdated JSON Schema example and assuming draft-07 is still the default everywhere.
- Letting every subsystem branch on old versions instead of normalizing once near the loader.
- Keeping support for old versions forever because no deprecation deadline was defined.
Conclusion
The best schema versioning strategy for JSON configuration files is usually not elaborate. Use an explicit `schemaVersion`, validate with a schema for that exact version, migrate incrementally to one latest internal model, and roll out reader support before new writers go live. That approach stays understandable for humans, testable for tooling, and much safer when real deployments drift over time.
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool