npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@louise-life/json-shape-compat

v0.1.1

Published

Check JSON backwards compatibility by inferring schemas from actual JSON samples

Readme

json-shape-compat

Check JSON backwards compatibility by inferring the shape from actual JSON samples — no schema definitions, no annotations, no decorators. Point it at the JSON your service produced yesterday and the JSON it produces today, and it tells you what broke.

The intended workflow: your CI saves a shape snapshot on every deploy. The next deploy compares the new response against that snapshot. If the diff contains breaking changes, CI fails.

Install

npm install -g json-shape-compat       # CLI
npm install --save-dev json-shape-compat  # library

CLI

Compare two documents

json-shape-compat compare old.json new.json
json-shape-compat old.json new.json    # shorthand

Exit codes:

  • 0 — no breaking changes
  • 1 — breaking changes detected
  • 2 — CLI / IO error

Snapshot workflow

# On deploy: capture the shape of a known-good response.
curl https://api.example.com/v1/users > sample.json
json-shape-compat snapshot sample.json -o shape.json

# On next deploy: check the new response against the snapshot.
curl https://api.example.com/v1/users > new.json
json-shape-compat check shape.json new.json

The snapshot is plain JSON — commit it to your repo.

Reading from stdin

Any input path can be -:

curl https://api.example.com/v1/users \
  | json-shape-compat check shape.json -

Output

JSON by default — --format text for terminal-friendly output.

json-shape-compat compare old.json new.json
# {
#   "breaking": [
#     { "path": "user.email", "type": "removed", "breaking": true,
#       "detail": "required property removed" }
#   ],
#   "nonBreaking": [
#     { "path": "user.createdAt", "type": "added", "breaking": false,
#       "detail": "new required property added (string)" }
#   ]
# }

json-shape-compat compare old.json new.json --format text
# BREAKING CHANGES (1):
#   [removed] user.email: required property removed
#
# non-breaking changes (1):
#   [added] user.createdAt: new required property added (string)

Pipe-friendly:

json-shape-compat compare old.json new.json | jq '.breaking | length'

Full help

json-shape-compat <command> [options]
json-shape-compat <old.json> <new.json>   (shorthand for: compare)

COMMANDS
  compare <old> <new>            Diff two JSON documents
  snapshot <input> -o <file>     Write an inferred shape to a file
  check <shape> <new>            Diff a shape snapshot against a new document

OPTIONS
  -f, --format <json|text>       Output format (default: json)
  -o, --out <path>               Output path (snapshot only)
  -h, --help                     Show help (use with a command for details)
  -V, --version                  Print version

Library

import { inferShape, inferShapeFromSamples, checkCompat } from "json-shape-compat";

const oldShape = inferShape(oldJson);
const newShape = inferShape(newJson);
const result = checkCompat(oldShape, newShape);

result.breaking;     // BreakingChange[]
result.nonBreaking;  // NonBreakingChange[]

Multiple samples

A single sample undercounts optional fields — anything missing from that one document looks "removed" forever. Pass several representative samples to capture which properties are sometimes-present:

const shape = inferShapeFromSamples([sample1, sample2, sample3]);
// or:
const shape = inferShape([sample1, sample2, sample3], { samples: true });

inferShapeFromSamples merges across samples: properties present in every sample are required: true, properties in only some are required: false, and divergent types widen into a union.

Change records

Each entry in breaking / nonBreaking is a Change:

interface Change {
  path: string;       // dot/bracket path, e.g. "posts[].author.email"
  type:
    | "removed" | "added" | "typeChanged"
    | "narrowed" | "widened" | "optional" | "required";
  breaking: boolean;
  detail: string;     // human-readable explanation
}

What counts as breaking

| Change | Breaking? | | ----------------------------------------------- | --------- | | Property removed | yes | | Property type changed (e.g. stringnumber) | yes | | Object replaced with primitive (or vice versa) | yes | | Array element type changed incompatibly | yes | | Property required → optional | yes | | Union variant dropped (type narrowed) | yes | | Property added | no | | Property optional → required | no | | Type widened (e.g. stringstring \| null) | no | | Array element gained new optional properties | no |

The asymmetry is from the consumer's perspective: anything a downstream parser might have relied on becoming unavailable is breaking. The producer emitting more than before is not.

How it works

inferShape(value) walks any JSON value once and builds a type tree:

  • Primitives → { kind: "primitive", type: "string" | "number" | "boolean" | "null" }
  • Objects → { kind: "object", properties: { name: { shape, required } } }
  • Arrays → { kind: "array", element: <merged union of all elements> }
  • Merged variants of different kinds → { kind: "union", variants }

checkCompat(old, new) walks both shapes in parallel and emits one Change per divergence, with the full JSON path.

There's no schema language to learn and no annotations to maintain. The spec for what your API returns is just the JSON it actually returns.

Limitations

  • Renames look like add + remove. A property going from name to displayName reports two changes: the removal is breaking, the addition is non-breaking. The tool can't infer intent.
  • String formats aren't checked. "2026-01-01" and "hello" are both string. If you change a UUID field to a free-form name, the tool sees no change.
  • Number ranges aren't checked. An int-only field becoming a float field is invisible to the tool — both are number.
  • Empty arrays infer to array<unknown>. Compatibility against an unknown element type always passes. Feed in samples with non-empty arrays, or merge multiple samples.

License

MIT