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

@statedelta/schema-checker

v0.1.0

Published

JSON Schema compatibility matching — decide whether schemaA ⊆ schemaB (subschema / containment). Sound, fast, zero-dependency. For type-safe port connections, breaking-change detection and data-contract validation.

Downloads

37

Readme

@statedelta/schema-checker

JSON Schema compatibility matching — decide whether schemaA ⊆ schemaB (subschema / containment), soundly and fast.

match(A, B) answers one question:

Is every JSON document valid against A also valid against B? (A ⊆ B)

That makes it the type-checker for connections between typed data ports — the foundation of a graph-based nocode backend: an output port (schema A) may feed an input port (schema B) only when A ⊆ B. It also detects breaking changes in contract/entity evolution, validates DTO pipelines, and gates service/saga wiring — anywhere you need to know, before data flows, that two shapes are compatible.

Unlike mainstream nocode tools (n8n, Zapier, Make, Node-RED), which are dynamically typed and defer compatibility to runtime, schema-checker brings real schema containment (à la IBM jsonsubschema / Confluent Schema Registry) to JavaScript, over JSON Schema Draft-07.

  • Sound — a compatible verdict is a proof you can trust (validated at 1,000,000+ random pairs against Ajv: zero unsound).
  • Tri-state & fail-closedcompatible | incompatible | unknown; never throws, never hangs.
  • Fast — polynomial in schema size; entity/aggregate checks are sub-millisecond.
  • Zero runtime dependencies.

Install

pnpm add @statedelta/schema-checker

Quick start

import { match, isSubschema } from "@statedelta/schema-checker";

// Producer output → consumer input. Safe to connect iff A ⊆ B.
const out = { type: "integer", minimum: 18, maximum: 65 };
const in_ = { type: "number", minimum: 0 };

match(out, in_).verdict; // "compatible"  (every int in [18,65] is a number ≥ 0)
isSubschema(out, in_);   // true

// Breaking-change direction:
match({ type: "number" }, { type: "integer" }).verdict; // "incompatible"

The result model — tri-state, sound, fail-closed

type MatchVerdict = "compatible" | "incompatible" | "unknown";

interface MatchResult {
  verdict: MatchVerdict;
  reasons: Reason[]; // empty when compatible
}

| Verdict | Meaning | Your gate | | --- | --- | --- | | compatible | Proven A ⊆ B. Safe to connect. | allow | | incompatible | Proven A ⊄ B — there is a value of A that B rejects. | block (real type error) | | unknown | Could not be decided (a construct outside the decidable profile). | block (fail-closed) — show a warning |

Why tri-state? The engine is sound, not complete: it prefers unknown over a wrong compatible. For a security gate, treat anything that is not compatible as a block — that is exactly isSubschema (below). incompatible vs unknown is a UX distinction: "this is wrong" vs "I can't prove it".

match never throws and never hangs: any internal error is caught and returned as unknown; a degenerate schema is bounded by a step budget and returns unknown. The gate fails closed.


API

match(source, target, opts?) → MatchResult

Does source ⊆ target? Returns the rich tri-state result with reasons. source is the producer/output (A); target is the consumer/input (B).

const r = match(producerSchema, consumerSchema, { registry });
if (r.verdict === "compatible") connect();
else showReasons(r.reasons);

isSubschema(source, target, opts?) → boolean

Boolean convenience — true only when provably compatible (unknownfalse, fail-closed). This is the recommended gate:

if (isSubschema(out, in_, opts)) proceed();
else block();

isCompatible(out, in, opts?) → MatchResult

Port-semantics alias of match — reads naturally for out → in wiring.

validateProfile(schema) → { ok, violations }

Lints a schema against the supported decidable subset. Run this at authoring time (e.g. in your editor) so users get a clear error instead of a silent unknown later.

const { ok, violations } = validateProfile(schema);
if (!ok) reportToAuthor(violations); // e.g. used `oneOf`, `not`, fractional multipleOf, remote $ref

createRegistry(entities) → Registry

Builds an in-memory registry of named entities so schemas can reference each other by $ref. Required whenever your schemas use references.

import { match, createRegistry } from "@statedelta/schema-checker";

(See Entities & references below.)

Options — MatchOptions

interface MatchOptions {
  registry?: Registry;                  // resolves $ref (entities)
  onUnsupported?: "unknown" | "throw";  // default "unknown"; "throw" surfaces internal errors (tests/CI)
}

Port semantics (variance) — the one rule not to get wrong

Connecting output(A) → input(B) is safe iff A ⊆ B: the producer must be narrower than or equal to what the consumer accepts.

  • property values & array items are covariant: A.prop ⊆ B.prop.
  • a consumer that requires a field rejects producers that don't guarantee it.
  • a closed consumer (additionalProperties: false) rejects a producer that can emit extra keys.
// adding a required field to the consumer is a breaking change:
const oldProducer = { type: "object", properties: { id: { type: "integer" } }, required: ["id"] };
const newConsumer = { type: "object", properties: { id: { type: "integer" }, tenantId: { type: "integer" } }, required: ["id", "tenantId"] };
isSubschema(oldProducer, newConsumer); // false — producer lacks tenantId

Entities & references

Declare entities once and reference them by $ref — the natural DDD shape (entities, value objects, aggregates). Recursive and mutually-recursive entities are fully supported (termination is guaranteed).

import { match, createRegistry } from "@statedelta/schema-checker";

const defs = {
  Email: { type: "string", minLength: 3, maxLength: 254 },
  User: {
    type: "object",
    properties: {
      id: { type: "integer", minimum: 1 },
      name: { type: "string" },
      email: { anyOf: [{ $ref: "#/definitions/Email" }, { type: "null" }] }, // nullable VO
      manager: { $ref: "#/definitions/User" }, // recursive — fine
    },
    required: ["id", "name"],
  },
  Admin: {
    type: "object",
    properties: {
      id: { type: "integer", minimum: 1 },
      name: { type: "string" },
      email: { $ref: "#/definitions/Email" }, // required, non-null
      role: { const: "admin" },
    },
    required: ["id", "name", "email", "role"],
  },
};

const registry = createRegistry(defs);
const ref = (n: string) => ({ $ref: `#/definitions/${n}` });

match(ref("Admin"), ref("User"), { registry }).verdict; // "compatible" (Admin specializes User)
match(ref("User"),  ref("Admin"), { registry }).verdict; // "incompatible"

Aggregates compose naturally and stay covariant:

// An Order whose customer is an Admin ⊆ an Order whose customer is a User.

Reasons & error codes

Every incompatible/unknown result carries structured reasons with a stable, machine-readable code — switch on code, never parse message (which is for display / i18n):

interface Reason {
  verdict: "incompatible" | "unknown";
  code: ReasonCode;     // e.g. "object/required", "number/minimum", "undecidable/union"
  schemaPath: string;   // JSON Pointer, e.g. "/properties/age"
  keyword?: string;     // the JSON Schema keyword at fault
  message: string;      // human-readable
  source?: JSONValue;   // the producer value/constraint
  target?: JSONValue;   // the consumer value/constraint
}
const r = match(a, b, opts);
for (const reason of r.reasons) {
  switch (reason.code) {
    case "object/required": highlightMissingField(reason.target); break;
    case "number/minimum":  highlightRange(reason.schemaPath); break;
    case "undecidable/union": showWarning("can't verify — your responsibility"); break;
    // …
  }
}

Codes are namespaced <category>/<detail>. undecidable/*, unsupported/*, ref/unresolved, and error/internal always mean verdict unknown; the rest mean incompatible. (Full list in docs/ARCHITECTURE.md §8.)


The supported profile (Draft-07 subset)

The engine targets a decidable, well-behaved subset of Draft-07 — exactly what typed data ports need. Supported: type (incl. nullable unions), enum, const, number ranges + multipleOf (integers), string length + pattern + format, arrays (items list-form, length, uniqueItems), objects (properties, required, additionalProperties, cardinality), allOf/anyOf, and $ref to a registry.

Out of profile (rejected by validateProfile, treated as unknown if they slip in): oneOf, not, if/then/else, fractional multipleOf, remote $ref. Conservatively unknown (sound, future increments): tuple arrays, multi-branch right unions, patternProperties/propertyNames/contains, divergent regex pattern.

No coercion, by design. Schema containment compares what a value is, not what it could be converted to. integer ⊆ number is genuine subtyping; string ↔ number would be coercion and is out of scope.

See docs/IMPLEMENTATION-PROPOSAL.md §5 for the full profile and decision log.


Guarantees (production)

  • Soundnesscompatible is a proof of A ⊆ B. Validated against Ajv at 1,000,000 self-contained pairs + 100,000 entity/ref pairs + a full DDD scenario suite: zero unsound verdicts.
  • Bounded time — comparison is polynomial in schema size (identity coinduction + memoization); an absolute step budget caps even adversarial input → unknown. No hang / DoS.
  • Fail-closedmatch never throws; isSubschema returns false on anything not provably compatible.

For the algorithms and the safety analysis, see docs/ARCHITECTURE.md.


Types

export type Schema = boolean | SchemaObject;
export type JSONType = "null" | "boolean" | "integer" | "number" | "string" | "array" | "object";
export type JSONValue = /* any JSON value */;

export type MatchVerdict = "compatible" | "incompatible" | "unknown";
export interface MatchResult { verdict: MatchVerdict; reasons: Reason[]; }
export interface Reason { verdict; code; schemaPath; keyword?; message; source?; target?; }
export interface ProfileResult { ok: boolean; violations: Reason[]; }

export interface Registry { resolve(ref: string, baseUri?: string): Schema | undefined; }
export interface MatchOptions { registry?: Registry; onUnsupported?: "unknown" | "throw"; }

export function match(source: Schema, target: Schema, opts?: MatchOptions): MatchResult;
export function isSubschema(source: Schema, target: Schema, opts?: MatchOptions): boolean;
export function isCompatible(out: Schema, in_: Schema, opts?: MatchOptions): MatchResult;
export function validateProfile(schema: Schema): ProfileResult;

Development

pnpm install
pnpm test          # unit + laws + differential (Ajv) + DDD scenarios + ref stress (100×100)
pnpm test:stress   # heavy tiers: 1,000,000 self-contained + 100,000 entity-ref pairs
pnpm typecheck
pnpm lint
pnpm build         # esm + cjs + d.ts (tsup)

License

MIT © Anderson D. Rosa