Classify JSON Schema and OpenAPI changes as breaking, compatible or informational for CI gates (MIT, free; pipx install) https://errant.solutions/tools/json-schema-diff.html
  • Python 91.1%
  • Shell 8.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-09 11:47:31 -08:00
.forgejo/workflows mutation/chaos CI on every rw-man host again (owner decision 2026-10-18: colo backlog too big, GuardDuty suppression kept): EC2 guards are opt-in (refuse only with CI_COLO_ONLY=1, code kept), colo jobs move to the shared labels mutation-x86_64 / mutation-any (rw-man-01..05, 6g slots) 2026-10-18 13:45:36 -07:00
ci ci: raise mutation floor 86 -> 87 (measured full run 87.05%, 317 survivors). The shared baseline on the mutation-baseline branch held survivors=[] (written by a skip run, last_full null), so every incremental run treated all 2447 touched-function mutants as NEW (run 62: 317 'new'); a green full run records the survivor list 2026-11-09 11:47:31 -08:00
tests tests: direct tests of the small helpers (pointer escape, canonical/hashable, deref, body/response extraction, Markdown 'No differences', predicates, Include expansion, RSA bits) so the full mutation run clears its floor again (nightly full runs: json 85.82% vs 86, sshd 77.5-78.4% vs 79) 2026-11-06 11:31:07 -08:00
.gitignore nightly mutation: incremental (only functions changed since the last green run) except a weekly full run on the repo's own weekday; replace-queued concurrency; staggered cron 2026-10-18 12:22:51 -07:00
CHANGELOG.md 1.3.1: document --fail-on-breaking in --help (alias of the default gate); test 2026-10-28 05:32:51 -07:00
claims.json 1.3.0: follow $refs to other local files (common.json#/$defs/X, refs between files, split OpenAPI specs); remote refs still never fetched 2026-10-18 14:27:12 -07:00
LICENSE Add MIT license 2026-10-16 14:02:01 -07:00
new_schema_breaking.json json-schema-diff: JSON Schema breaking-change detector for API versioning 2026-09-17 01:38:06 -07:00
new_schema_compatible.json json-schema-diff: JSON Schema breaking-change detector for API versioning 2026-09-17 01:38:06 -07:00
old_schema.json json-schema-diff: JSON Schema breaking-change detector for API versioning 2026-09-17 01:38:06 -07:00
pyproject.toml Mutation testing: copy all of ci/ into mutmut's sandbox (also_copy of ci/check_claims.py failed: no mutants/ci dir), keep real sleeps in fixture-server threads (--timeout test), per-repo mutant parallelism (ci/mutation-children) 2026-10-16 23:56:28 -07:00
README.md 1.3.1: document --fail-on-breaking in --help (alias of the default gate); test 2026-10-28 05:32:51 -07:00
schema_diff.py 1.3.1: document --fail-on-breaking in --help (alias of the default gate); test 2026-10-28 05:32:51 -07:00

json-schema-diff

A JSON Schema breaking-change detector, built for API versioning.

Compares two JSON Schema documents -- or two OpenAPI 3.x / Swagger 2.0 specs -- (old vs. new) and classifies every difference as BREAKING, compatible, or info — so you can catch an accidental breaking change to your API contract before you ship it, instead of finding out from an angry integration partner.

Zero dependencies (Python stdlib only — json + argparse), no network calls, works entirely offline. Supports the common subset of JSON Schema (draft-07 / 2020-12) actually used in real API schemas: type, properties, required, additionalProperties, enum, items, format, minimum/maximum, exclusiveMinimum/exclusiveMaximum, minLength/maxLength, minItems/maxItems, pattern, const, oneOf/anyOf/allOf/not, nullable (OpenAPI 3.0), dependentRequired/dependencies, and local $refs (any JSON pointer: #/definitions/..., #/$defs/..., #/components/schemas/..., including recursive schemas), which are followed so a change inside a shared definition is reported at every place it's used. $refs to other files work the same way (common.json#/$defs/Id, ./schemas/user.json, resolved relative to the file that contains them, also between files and in OpenAPI specs split into several files); remote http(s):// refs are never fetched. Python 3.8+. Specs in YAML need converting to JSON first (e.g. yq -o json spec.yaml > spec.json).

What it catches

  • Type narrowing — ["string","integer"] -> "string" or number -> integer (BREAKING), a type added where there was none (BREAKING), or a straight incompatible type swap (BREAKING); integer -> number is a widening (compatible)
  • Newly-required fields — a field that used to be optional is now required; existing producers that omit it will fail validation (BREAKING)
  • Removed required fields — a required field disappears entirely; any consumer depending on it breaks (BREAKING)
  • additionalProperties tightened to false — previously-accepted extra fields get rejected (BREAKING); removing an optional field from an object with additionalProperties: false is BREAKING too (documents still sending it are rejected), while in an open object it's compatible
  • Enum values removed — previously-valid values now rejected (BREAKING)
  • Numeric/length constraints narrowed — minimum raised, maximum lowered, maxLength shortened, etc. (BREAKING); the reverse relaxations are flagged as compatible. minimum/exclusiveMinimum (and the maximum pair) are judged together, so exclusiveMinimum: 0 -> minimum: 0 is a relaxation
  • Pattern changed — treated conservatively as BREAKING unless it's unchanged, since proving one regex is a strict superset of another is out of scope for a stdlib tool
  • Everything with the opposite effect (new optional fields, relaxed constraints, added enum values, widened types, removed constraints) is classified compatible
  • format hint changes are info-only, since most validators only warn on format anyway
  • Composition: a oneOf/anyOf variant removed (BREAKING) or added (anyOf: compatible; oneOf: info, because an overlapping new variant makes some documents match twice), an allOf subschema added (BREAKING) or removed (compatible), and changes inside a variant -- variants are paired by $ref name, title, const/discriminator value or type
  • const added or changed (BREAKING), null no longer accepted (nullable/type: null removed: BREAKING), new dependentRequired entries (BREAKING)

OpenAPI mode

Detected automatically from the openapi/swagger key. It compares paths and operations (removed endpoint/operation: BREAKING), parameters (new required parameter, parameter became required, narrower parameter schema: BREAKING), request bodies (became required, content type dropped, narrower schema: BREAKING) and responses (2xx response removed: BREAKING). Request schemas are judged from the client's side (a narrower schema rejects requests that used to work) and response schemas from the consumer's side: a new enum value, a field no longer in required, a removed field or a widened type in a response breaks clients, while a new optional response field doesn't.

Install

pipx install git+https://git.canutethegreat.com/CanuteTheGreat/json-schema-diff.git        # latest release branch; puts `json-schema-diff` on PATH
pipx install git+https://git.canutethegreat.com/CanuteTheGreat/json-schema-diff.git@v1.3.1  # a pinned release
brew install canutethegreat/errant-tools/json-schema-diff   # after: brew tap canutethegreat/errant-tools https://git.canutethegreat.com/CanuteTheGreat/homebrew-errant-tools
scoop install errant-tools/json-schema-diff                 # after: scoop bucket add errant-tools https://git.canutethegreat.com/CanuteTheGreat/scoop-errant-tools

Or download the single file (stdlib-only Python 3.8+) and run it directly: curl -fsSLO https://errant.solutions/downloads/schema_diff.py && python3 schema_diff.py --help. Every release has a changelog, the wheel, the sdist and a SHA256SUMS file. Not on PyPI yet, so plain pipx install json-schema-diff doesn't work until it's published there.

Usage

json-schema-diff old_schema.json new_schema.json
json-schema-diff old_schema.json new_schema.json --json
json-schema-diff openapi-v1.json openapi-v2.json                 # OpenAPI/Swagger, detected automatically
json-schema-diff old.json new.json --markdown >> CHANGELOG.md     # changelog section
json-schema-diff old.json new.json --fail-on never                # report only

Exit codes (same convention as every Errant Solutions tool):

Code Meaning
0 nothing at or above --fail-on (default breaking)
1 at least one BREAKING change (or any compatible change with --fail-on compatible)
2 usage/parse error, or one file is OpenAPI and the other a plain schema

1.x note: the tool used to exit 0 unless --fail-on-breaking was given; it now gates on BREAKING by default (--fail-on-breaking is still accepted).

Why this exists

Most API teams find out about a breaking schema change when a downstream consumer's integration starts failing in production — usually days after the change shipped, and often traced back to something as small as a field quietly becoming required or an enum value getting dropped. Running this in CI against the last-released schema and the current one turns that into a merge-time check instead of a customer-facing incident.

License / pricing

MIT — see LICENSE. Free; pay what you want if it caught a breaking change for you: https://errant.solutions (payment link on the site).

Tests

tests/ checks every classification against a real validator (jsonschema): each validation-level BREAKING finding comes with a document the old schema accepts and the new one rejects, and tests/test_accuracy.py runs ~30 single-keyword edits in both directions (request and response) against a document pool, failing on any false negative or false positive. The contract conventions above (a removed required field, a removed response field, a new optional field, extra response fields) are asserted explicitly there. OpenAPI mode is covered with 3.0 and Swagger 2.0 fixtures. CI runs it on Python 3.8 and current Python, then installs the package with pip and checks --version/--help.