Can new code read old data?
Everything valid under the previous schema must stay valid under the current one.
Tightening maxLength breaks it.
v0.1.0 · JSON Schema and PostgreSQL DDL
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-diffSB-S005public.users.planNOT NULL column public.users.plan (text) was added without a default.
Real output from compareSchemasSync. Nothing executed, no database touched.
Directional compatibility
A change can be safe for new code and fatal for old code, or the other way round. Each finding carries both verdicts.
Everything valid under the previous schema must stay valid under the current one.
Tightening maxLength breaks it.
Everything valid under the current schema must be valid under the previous one.
Adding a SQL column breaks it.
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
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.
Changes become expand, migrate and contract steps. Backfill values are never invented; you get a commented template.
Findings never change. Your policy sets severity, so one report can gate a deploy strictly and a review loosely.
Properties, requiredness, types, nullability, enums, bounds, patterns, $ref and composition, with local $ref resolved.
Tables, columns, constraints, indexes and enums. SQL is generated for PostgreSQL only; other dialects are rejected, not guessed.
Stable rule IDs, deterministic JSON and exit codes 0, 1 and 2. Same inputs and policy always give the same report.
Staged migration plans
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.
Additive steps that are safe to ship first.
ALTER TABLE "public"."users" ADD COLUMN "plan" text;Deploy code that populates the column on every insert/update.
Backfills and verification of existing data.
-- UPDATE "public"."users" SET "plan" = /* value you must choose */ WHERE "plan" IS NULL;Tighten or remove once nothing depends on the old shape.
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
The format is inferred from the extension. Exit codes tell your pipeline what happened.
# 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 errorPass paths, raw text or parsed values. The report is plain data you can filter and test.
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);
}Check out the previous schema, compare, and let the exit code decide.
- 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{
"format": "postgres-ddl",
"policy": {
"compatibility": "backward",
"destructiveChanges": "error",
"failOn": "error",
"rules": { "SB-S019": "off" }
},
"renames": {
"columns": { "users.fullname": "full_name" }
}
}Playground
This runs the schemabridge-diff source from this repository in your browser. Nothing is mocked and nothing is uploaded.
SB-S005column-added-not-null-no-defaultpublic.users.plan
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 →
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
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.Documentation
More from M2Toolkit