jsoncompat 0.4.2

JSON Schema and OpenAPI Compatibility Checker
Documentation
[![jsoncompat logo](web/jsoncompatdotcom/public/logo192.png)](https://jsoncompat.com)

# jsoncompat

[![crates.io](https://img.shields.io/crates/v/jsoncompat)](https://crates.io/crates/jsoncompat) [![docs.rs](https://docs.rs/jsoncompat/badge.svg)](https://docs.rs/jsoncompat) [![PyPI](https://img.shields.io/pypi/v/jsoncompat.svg)](https://pypi.org/project/jsoncompat/) [![npm](https://img.shields.io/npm/v/jsoncompat.svg)](https://www.npmjs.com/package/jsoncompat) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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.

If a schema declares `$schema`, it must use Draft 2020-12 or the OpenAPI 3.1 Schema Object dialect. OpenAPI 3.0-only shortcuts such as `nullable` are not reinterpreted.

> [!WARNING]
> jsoncompat is alpha software. It is intentionally conservative in places, and it can still miss incompatible changes or report false positives.
>
> The full docs and examples live at [jsoncompat.com]https://jsoncompat.com.

## Install

Install the CLI with Cargo:

```bash
cargo install jsoncompat
```

Python and JavaScript/WebAssembly packages are documented separately:

- [Python bindings]pybindings/README.md
- [JavaScript/WebAssembly bindings]wasm/README.md

## Quick start

Check a serializer-facing schema change:

```bash
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:

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

Check an OpenAPI 3.1 contract:

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

Generate example values accepted by a schema:

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

Compare schema golden files in CI:

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

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

```bash
jsoncompat lower-openapi openapi.json
```

Run the guided CLI demo:

```bash
jsoncompat demo --noninteractive
```

## Stamped schemas

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

```json
{
  "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.

```bash
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. Importing a generated module only defines ordinary
dataclasses. The first constructor or conversion call derives one shared native
plan from their field metadata and caches it on every generated class in the
module; there is never a reflective per-value or Python-constructor fallback.
The JSON Schema validator is compiled separately on the first checked use, so
`skip_validation=True` does not pay validator startup cost. Generated classes
carry the original input schema in `__jsoncompat_schema__` and expose:

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

- `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](examples/dataclasses/demo.py) for an
ordinary model that both serializes and deserializes. The
[canonical stamped-schema example](examples/stamp/demo.py) 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.

## 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](openapi/README.md) for the OpenAPI user guide.

## Rust API

Schema compatibility:

```rust
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:

```rust
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());
```

The Rust API also exposes structured compatibility errors, OpenAPI issue reports, incompatibility explanations, and schema-guided value generation.

## Warnings and hard errors

jsoncompat keeps warnings and hard errors separate:

- unsupported-but-valid schema details produce warnings and the modeled comparison continues;
- inputs that would make a verdict unsafe fail before comparison;
- unsupported OpenAPI contract surfaces fail before comparison rather than being silently ignored.

The CLI prints warnings with exact pointers so you can see what was ignored. See [developing.md](developing.md) for the detailed support boundaries and the reasoning behind them.

## What to read next

- [jsoncompat.com]https://jsoncompat.com for polished documentation and examples
- [openapi/README.md]openapi/README.md for OpenAPI-specific usage
- [developing.md]developing.md for repository layout, internals, tests, fixtures, benchmarks, and release notes
- [docs.rs]https://docs.rs/jsoncompat for the Rust API reference

## License

MIT License. See [LICENSE](LICENSE).