jsoncompat 0.5.0

JSON Schema and OpenAPI Compatibility Checker
Documentation

jsoncompat logo

jsoncompat

crates.io docs.rs PyPI npm License: MIT

Check whether evolving JSON Schemas and OpenAPI 3.1 contracts stay backward-compatible.

jsoncompat compares:

  • raw JSON Schema Draft 2020-12 documents;
  • OpenAPI 3.1 Schema Objects;
  • JSON OpenAPI 3.1 documents with path operations.

By default, $schema must use Draft 2020-12 or the OpenAPI 3.1 Schema Object dialect. Rust callers can supply Draft 2020-12 vocabulary metaschemas and external resources through SchemaOptions. OpenAPI 3.0-only shortcuts such as nullable are not reinterpreted.

[!WARNING] jsoncompat is alpha software. It is intentionally conservative: incomplete proofs without validated counterexamples are reported as Unknown.

The full docs and examples live at jsoncompat.com.

Install

Install the CLI with Cargo:

cargo install jsoncompat

Python and JavaScript/WebAssembly packages are documented separately:

Quick start

Check a serializer-facing schema change:

jsoncompat compat old-schema.json new-schema.json --role serializer

Check both serializer and deserializer compatibility, and ask for fuzzed counterexamples when static analysis finds a problem:

jsoncompat compat old-schema.json new-schema.json --role both --fuzz 1000 --depth 8

Use --json for a structured verdict. Exit codes are 0 for compatible, 1 for incompatible or invalid input, and 2 for unknown.

Check an OpenAPI 3.1 contract:

jsoncompat compat --openapi old-openapi.json new-openapi.json

Generate example values accepted by a schema:

jsoncompat generate schema.json --count 5 --pretty

Compare schema golden files in CI:

jsoncompat ci old-golden.json new-golden.json --display table

Inspect the per-operation request and response schemas generated from an OpenAPI document:

jsoncompat lower-openapi openapi.json

Run the guided CLI demo:

jsoncompat demo --noninteractive

Stamped schemas

jsoncompat stamp turns a schema into separate writer and reader schemas using a versioned envelope:

{
  "version": 2,
  "data": {
    "name": "Ada"
  }
}

Writers emit only the latest schema version, while readers accept a tagged union of historical writer versions. The command stores schema history in a manifest file and appends a new version whenever a change is not compatible in both directions.

jsoncompat stamp --manifest schemas.manifest.json --id user-profile --write-manifest schema.json
jsoncompat stamp --manifest schemas.manifest.json --id user-profile --display writer schema.json > writer.schema.json
jsoncompat stamp --manifest schemas.manifest.json --id user-profile --display reader schema.json > reader.schema.json
jsoncompat codegen --target schema reader.schema.json
jsoncompat codegen --target dataclasses reader.schema.json > reader_models.py

Dataclass code generation

jsoncompat codegen --target dataclasses accepts any JSON Schema document, canonicalizes it with SchemaDocument::canonical_schema_json(), and emits frozen, slotted Python dataclasses backed by one native construction and serialization runtime. Generation resolves model types and references, compiles validation programs and regexes, and computes conversion optimizations. Import creates classes and binds them to those prebuilt programs; first use performs no model compilation. Model.__jsoncompat_schema__ exposes the input schema; the generated artifact stores it compressed and expands it only on explicit access. Validation and error reporting use the prebuilt native program.

jsoncompat codegen --target dataclasses schema.json > models.py

Code generation also prepares dataclass methods, conversion plans, validation programs, and regexes. There is no separate model build command and no deferred schema compilation at import or first use.

To keep the public API easy to read, write a model module and its private companion:

jsoncompat codegen --target dataclasses schema.json --output models.py

models.py imports _models_generated.py at the top, then declares the root model, its fields, and the other public classes. It binds the classes at the bottom. Regeneration replaces these same two files; deploy them together. Output to stdout remains self-contained, with the generated implementation below the public declarations. Both layouts use the same prepared runtime. Import creates classes, loads precomputed programs, and binds slots; full dataclass reflection metadata is created only when explicitly requested. Constructors and JSON I/O need no further preparation. See generated Python models for the build contract and benchmarks.

Generate an explicitly selected OpenAPI component package in the same build step:

jsoncompat codegen --openapi openapi.json --output my_models --models-per-module 128

Use from my_models import Order. The package loads model modules on demand; reference-connected models share identities and independent models stay in bounded modules. --component selects roots and --rename resolves name collisions. See OpenAPI model packages.

  • from_value(...) / to_value(...) for schema-checked conversion between generated models and Python JSON values;
  • deserialize(...) / serialize(...) for JSON, YAML, and MessagePack wire formats, with JSON as the default;
  • keyword-only skip_validation=True on direct construction and every conversion method when the caller can guarantee the value is schema-valid;
  • __jsoncompat_extra__ for schema-admitted object properties that are not declared under properties, including additionalProperties and patternProperties;
  • JSONCOMPAT_MISSING for omitted optional fields so absent and explicit null stay distinguishable.

When the schema structure makes it honest, code generation also keeps Python annotations narrow rather than collapsing to Any, including primitive local $ref fields rooted under $defs or legacy definitions, plus constrained tuple-like arrays built from prefixItems.

If the input schema contains x-jsoncompat metadata from jsoncompat stamp, generated writer envelopes inherit from WriterDataclassModel, which disables deserialization methods, and generated reader envelopes inherit from ReaderDataclassModel / ReaderDataclassRootModel, which disable serialization methods.

skip_validation=True skips only the attached JSON Schema check. Wire-format parsing and JSON-value normalization, runtime type conversion, and reader/writer direction guards still apply.

The runtime contract applies only to dataclasses emitted by this command. Hand-written subclasses, custom __init__ / __new__ / __post_init__ hooks, inheritance between generated models, and Python default factories are not supported model-definition APIs. JSON Schema default remains an annotation and does not manufacture a Python value for a missing property.

Install optional codecs with jsoncompat[yaml] and jsoncompat[msgpack]. Every decoded format is restricted to JSON-compatible values before model construction; format-specific values such as YAML timestamps or MessagePack binary/extension values are rejected.

See the canonical plain-schema example for an ordinary model that both serializes and deserializes. The canonical stamped-schema example covers separate writer/reader envelopes and historical versions.

Choose a role

Compatibility is directional:

Role Question jsoncompat answers
serializer Can old readers still accept every value the new producer may emit?
deserializer Can the new reader still accept every value older producers may have emitted?
both Are both directions safe?

That is why making a previously required response field optional can be breaking for a serializer, while making a previously optional stored field required can be breaking for a deserializer.

Deserializer checks assume old producers omit optional undeclared object properties where the checker can establish that assumption safely. Required properties and dependency-forced properties still have their values checked. Complex applicator comparisons conservatively use full validation semantics.

OpenAPI contracts

When the inputs are OpenAPI documents, pass --openapi. jsoncompat compares:

  • path, query, header, and cookie parameters;
  • request bodies and media types;
  • response statuses, media types, bodies, and headers;
  • removed operations;
  • supported local #/components/... references.

Requests are checked in the deserializer direction. Responses are checked in the serializer direction. --role and --fuzz are raw-JSON-Schema-only flags.

See openapi/README.md for the OpenAPI user guide.

Rust API

Schema compatibility:

use jsoncompat::{Role, SchemaDocument, check_compat};
use serde_json::json;

let old = SchemaDocument::from_json(&json!({ "type": "string" })).unwrap();
let new = SchemaDocument::from_json(&json!({ "type": ["string", "null"] })).unwrap();

let compatible = check_compat(&old, &new, Role::Deserializer).unwrap();

OpenAPI compatibility:

use jsoncompat::{OpenApiDocument, check_openapi_compat};
use serde_json::json;

let old = OpenApiDocument::from_json(&json!({
    "openapi": "3.1.0",
    "info": { "title": "Pets", "version": "1.0.0" },
    "paths": {}
})).unwrap();
let new = old.clone();

let report = check_openapi_compat(&old, &new).unwrap();
assert!(report.is_compatible());

Use analyze_compat(&old, &new, role) for a three-way result: Compatible, Incompatible { direction, counterexample }, or Unknown { reason }. An incompatible result always includes a validated witness. The boolean check_compat API returns false when inclusion is not proved, including unknown cases. The CLI reports that distinction explicitly.

See the keyword support matrix for parsing, validation, proof, generation, resource-resolution, and format-vocabulary coverage.

Unknown results and errors

Malformed schemas, missing resources, unsupported dialects, and unsupported OpenAPI contract surfaces produce errors. Supported annotations do not produce warnings or affect compatibility. Complex valid schemas can yield an unknown proof result. See the keyword support matrix for precise boundaries.

What to read next

License

MIT License. See LICENSE.