v0.1.0 · JSON Schema and PostgreSQL DDL

Know who breaks before a schema change ships.

SchemaBridge compares two versions of a schema and reports, for every change, whether old data and new data still work, with a stable rule ID, a severity and a staged plan to get there safely. It never runs anything.

npm install --save-dev schemabridge-diff
previouscurrentblocked
  • errorSB-S005public.users.plan

    NOT NULL column public.users.plan (text) was added without a default.

    backwardincompatibleforwardincompatible

Real output from compareSchemasSync. Nothing executed, no database touched.

0runtime dependencies
Node ≥18Framework-agnostic · ESM, ships its own types
46stable rule IDs, never reused
0 · 1 · 2passed, violated, bad input

Directional compatibility

A diff tells you what changed. This tells you who it breaks.

A change can be safe for new code and fatal for old code, or the other way round. Each finding carries both verdicts.

Backward

Can new code read old data?

Everything valid under the previous schema must stay valid under the current one.

Tightening maxLength breaks it.

Forward

Can old code read new data?

Everything valid under the current schema must be valid under the previous one.

Adding a SQL column breaks it.

Unknown

When the schema can’t say.

A changed pattern, or removing a property from an open object. Never counted as breaking, never silently called safe.

Reported as a warning, with the reason.

What you get

One report your reviewer and your pipeline both trust.

  • Directional, not just “changed”

    Every finding says whether old data still reads under the new schema, and whether new data still reads under the old one. Unknown stays unknown.

  • Plans, not just warnings

    Changes become expand, migrate and contract steps. Backfill values are never invented; you get a commented template.

  • Policy decides the verdict

    Findings never change. Your policy sets severity, so one report can gate a deploy strictly and a review loosely.

  • JSON Schema

    Properties, requiredness, types, nullability, enums, bounds, patterns, $ref and composition, with local $ref resolved.

  • PostgreSQL DDL

    Tables, columns, constraints, indexes and enums. SQL is generated for PostgreSQL only; other dialects are rejected, not guessed.

  • Built for CI

    Stable rule IDs, deterministic JSON and exit codes 0, 1 and 2. Same inputs and policy always give the same report.

Staged migration plans

The same change, in the order that doesn’t hurt.

Adding a NOT NULL column without a default fails on a populated table. This is the plan SchemaBridge produced for that change, straight from the library.

Expand

Additive steps that are safe to ship first.

  1. 1Add public.users.plan as nullable
    ALTER TABLE "public"."users" ADD COLUMN "plan" text;
  2. 2Write public.users.plan from application code

    Deploy code that populates the column on every insert/update.

Migrate

Backfills and verification of existing data.

  1. 3Backfill public.users.plan
    -- UPDATE "public"."users" SET "plan" = /* value you must choose */ WHERE "plan" IS NULL;

Contract

Tighten or remove once nothing depends on the old shape.

  1. 4Enforce NOT NULL on public.users.plan
    ALTER TABLE "public"."users" ADD CONSTRAINT "users_plan_not_null" CHECK ("plan" IS NOT NULL) NOT VALID;
    ALTER TABLE "public"."users" VALIDATE CONSTRAINT "users_plan_not_null";
    ALTER TABLE "public"."users" ALTER COLUMN "plan" SET NOT NULL;
    ALTER TABLE "public"."users" DROP CONSTRAINT "users_plan_not_null";

Plans are suggestions for review. They can’t know your data volume, lock contention or deployment order. Read the limitations →

How it works

From the terminal, from code, from CI.

  1. 1

    Compare two files

    The format is inferred from the extension. Exit codes tell your pipeline what happened.

    sh
    # JSON Schema, inferred from .json
    schemabridge compare schema-v1.json schema-v2.json
    
    # PostgreSQL DDL, requiring both directions to hold
    schemabridge compare schema-v1.sql schema-v2.sql --compat full
    
    # For CI: structured output and a gating exit code
    schemabridge compare old.json new.json --json > report.json
    echo $?   # 0 passed · 1 policy violated · 2 input error
  2. 2

    Or call the API

    Pass paths, raw text or parsed values. The report is plain data you can filter and test.

    ts
    import { compareSchemas, formatReport, exitCodeFor } from "schemabridge-diff";
    
    const report = await compareSchemas({
      previous: "./schema-v1.json",
      current: "./schema-v2.json",
      policy: { compatibility: "backward", destructiveChanges: "error" },
      renames: { properties: { "$.user.name": "fullName" } },
    });
    
    console.log(formatReport(report));
    process.exitCode = exitCodeFor(report);
    
    for (const f of report.findings) {
      console.log(f.ruleId, f.severity, f.path, f.backward, f.forward);
    }
  3. 3

    Gate the pull request

    Check out the previous schema, compare, and let the exit code decide.

    yaml
    - name: Check schema compatibility
      run: |
        git show origin/main:schema/users.sql > /tmp/previous.sql
        npx schemabridge compare /tmp/previous.sql schema/users.sql \
          --config schemabridge.config.json
    json
    {
      "format": "postgres-ddl",
      "policy": {
        "compatibility": "backward",
        "destructiveChanges": "error",
        "failOn": "error",
        "rules": { "SB-S019": "off" }
      },
      "renames": {
        "columns": { "users.fullname": "full_name" }
      }
    }

Playground

Edit either side. The verdict follows as you type.

This runs the schemabridge-diff source from this repository in your browser. Nothing is mocked and nothing is uploaded.

Blocked1 errors · 0 warnings · 0 info
  • errorSB-S005column-added-not-null-no-default

    public.users.plan

    NOT NULL column public.users.plan (text) was added without a default.

    Impact The ALTER fails if the table has rows, and inserts from code that does not know the column will fail once it exists.

    Fix Add it nullable, deploy code that writes it, backfill existing rows in batches, then enforce NOT NULL.

    backwardincompatibleforwardincompatible

A required column with no default. The plan splits it into expand, migrate and contract, and leaves the backfill value for you to choose. How compatibility works →

The code behind this panel
ts
import { compareSchemasSync, type Format, type Policy, type Report } from "schemabridge-diff";

export type Input = {
  format: Format;
  previous: string;
  current: string;
  policy: Partial<Policy>;
};

/** Runs the library's in-memory entry point. Nothing is uploaded; the files never leave the page. */
export function compare({ format, previous, current, policy }: Input): Report {
  return compareSchemasSync({
    format,
    previous: { text: previous, name: "previous" },
    current: { text: current, name: "current" },
    policy,
  });
}

Policies

Findings stay the same. Severity is your call.

Pick how strict a run should be without changing what is detected.

  • --compat backwardOld data must stay valid. The default.
  • --compat forwardNew data must be readable by old consumers.
  • --compat fullBoth directions. Almost any real change fails.
  • --compat strictAssert two schemas are equivalent.
  • --compat noneList changes without ever failing a build.

See every policy option →

Documentation

Everything is written down.

More from M2Toolkit