Know when your APIs change before your users do.
API Schema Differentiator is a zero-config API schema drift detector. Point it at any API response β it automatically learns the schema, snapshots it, and alerts you when things change. No OpenAPI spec required. No manual schema writing. It just works.
- The Problem
- Installation
- Quick Start (30 seconds)
- Usage as a Library
- Usage as a CLI Tool
- Usage in Test Frameworks
- Usage in CI/CD Pipelines
- Watch Mode (Monitoring)
- Report Formats
- What Drift Gets Detected
- Multi-Sample Learning
- Supported Formats
- API Reference
- CLI Reference
- Configuration
Your app consumes /api/v2/users which returns:
{ "id": 123, "name": "Alice", "role": "admin" }The backend team deploys a refactor. Now it returns:
{ "id": "123", "name": "Alice", "roles": ["admin"] }Three things broke silently: id changed from number to string, role was renamed to roles, and its type changed from string to array. Nobody noticed until customers filed support tickets.
API Schema Differentiator catches that automatically.
npm install api-schema-differentiatorOr install globally to use the CLI anywhere:
npm install -g api-schema-differentiatorimport { SchemaGuard } from 'api-schema-differentiator';
const guard = new SchemaGuard({ store: './schemas' });
// Fetch your API
const response = await fetch('https://api.example.com/v2/users/1');
const data = await response.json();
// First run: auto-learns and saves the schema
// Every run after: compares against saved schema and reports drift
const report = await guard.check('GET /users/:id', data);
if (report.hasBreakingChanges) {
console.log(guard.format(report)); // pretty console output
throw new Error(`API schema broke! ${report.summary.breaking} breaking changes detected.`);
}# Step 1: Save a baseline schema from a response file
api-schema-differentiator snapshot --key "GET /api/users" --data response.json
# Step 2: Later, check a new response against the baseline
api-schema-differentiator check --key "GET /api/users" --data new-response.jsonOutput:
π Schema Drift Report: GET /api/users
ββββββββββββββββββββββββββββββββββββββββββββββββββ
π΄ BREAKING Field removed: "role" (was: string)
π΄ BREAKING Type changed at "id" (number β string)
π‘ WARNING Field possibly renamed: "role" β "roles"
π’ INFO Field added: "updated_at" (string)
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Summary: 2 breaking | 1 warnings | 1 info
Compatibility Score: 68%
import { SchemaGuard } from 'api-schema-differentiator';
// Create a guard with a local store directory
const guard = new SchemaGuard({ store: './schemas' });
// Check any data β objects, arrays, strings, JSON...
const report = await guard.check('my-endpoint', { id: 1, name: 'Alice' });
// Inspect the report
console.log(report.hasBreakingChanges); // true/false
console.log(report.compatibilityScore); // 0-100
console.log(report.summary.breaking); // number of breaking changes
console.log(report.changes); // detailed list of every changeCompare two response objects directly without saving anything:
const guard = new SchemaGuard({ store: './schemas' });
const before = { id: 1, name: 'Alice', role: 'admin' };
const after = { id: '1', name: 'Alice', roles: ['admin'] };
const report = guard.diffData(before, after);
console.log(guard.format(report)); // pretty-printed drift reportconst guard = new SchemaGuard({ store: './schemas' });
// Compare version 1 and version 3 of a saved schema
const report = await guard.diff('GET /users/:id', 1, 3);
console.log(guard.format(report, 'markdown'));Feed multiple responses to teach the tool which fields are always present (required) vs sometimes present (optional):
const guard = new SchemaGuard({ store: './schemas' });
// Sample 1: has email
await guard.learn('GET /users/:id', { id: 1, name: 'Alice', email: 'a@b.com' });
// Sample 2: no email
await guard.learn('GET /users/:id', { id: 2, name: 'Bob' });
// Now the schema knows "email" is optional, "id" and "name" are required
// Future checks won't flag missing "email" as a breaking changeconst guard = new SchemaGuard({
store: './schemas', // Where to save snapshots (directory path)
autoSnapshot: true, // Auto-save schema on first check (default: true)
autoUpdate: false, // Auto-update schema when drift detected (default: false)
minSeverity: 'warning', // Filter out 'info' changes from reports (default: 'info')
metadata: { // Custom metadata saved with each snapshot
team: 'backend',
environment: 'staging',
},
});const report = await guard.check('GET /users/:id', data);
// Console (colored terminal output)
console.log(guard.format(report, 'console'));
// JSON (machine-readable, perfect for CI)
fs.writeFileSync('report.json', guard.format(report, 'json'));
// Markdown (great for PR comments)
fs.writeFileSync('report.md', guard.format(report, 'markdown'));
// HTML (standalone report with dark theme UI)
fs.writeFileSync('report.html', guard.format(report, 'html'));Use the inference and diff engines directly:
import { inferSchema, diffSchemas, mergeSchemas, formatReport } from 'api-schema-differentiator';
// Infer a schema from any data
const schema = inferSchema({ id: 1, name: 'Alice', tags: ['admin'] });
// => { type: 'object', properties: { id: { type: 'number', ... }, ... } }
// Diff two schemas
const changes = diffSchemas(oldSchema, newSchema);
// Merge schemas from multiple samples
const merged = mergeSchemas(schema1, schema2);# From a file
api-schema-differentiator snapshot --key "GET /api/users" --data response.json
# From a specific store directory
api-schema-differentiator snapshot --key "GET /api/users" --data response.json --store ./my-schemas# Check and print to console
api-schema-differentiator check --key "GET /api/users" --data new-response.json
# Check with JSON output
api-schema-differentiator check --key "GET /api/users" --data new-response.json --format json
# Check and write report to file
api-schema-differentiator check --key "GET /api/users" --data new-response.json --format html --output report.html
# Fail on warnings (not just breaking)
api-schema-differentiator check --key "GET /api/users" --data new-response.json --fail-on warningExit codes:
0= No drift (or drift below the--fail-onthreshold)1= Drift detected at or above the--fail-onseverity
# Compare two response files
api-schema-differentiator diff --before old-response.json --after new-response.json
# Compare with markdown output
api-schema-differentiator diff --before v1.json --after v2.json --format markdown
# Compare two stored versions
api-schema-differentiator diff --key "GET /api/users" --v1 1 --v2 3api-schema-differentiator list --store ./schemasOutput:
π Monitored endpoints (3):
β’ GET /api/v2/users
Latest: v3 (2024-06-15T10:30:00Z)
Samples: 5
β’ POST /api/v2/orders
Latest: v1 (2024-06-10T08:00:00Z)
Samples: 1
β’ GET /api/v2/products
Latest: v2 (2024-06-14T15:00:00Z)
Samples: 3
api-schema-differentiator history --key "GET /api/v2/users" --store ./schemasOutput:
π Version history for "GET /api/v2/users":
v1 β 2024-06-01T10:00:00Z (1 samples)
v2 β 2024-06-08T10:00:00Z (3 samples)
v3 β 2024-06-15T10:00:00Z (5 samples)
# Poll every hour
api-schema-differentiator watch --url "https://api.example.com/v2/users/1" --interval 1h
# With auth headers
api-schema-differentiator watch \
--url "https://api.stripe.com/v1/charges" \
--header "Authorization: Bearer sk_test_..." \
--interval 6h
# With Slack webhook alerts
api-schema-differentiator watch \
--url "https://api.example.com/v2/users" \
--interval 30m \
--alert-webhook "https://hooks.slack.com/services/T.../B.../xxx"import { SchemaGuard } from 'api-schema-differentiator';
const guard = new SchemaGuard({ store: './api-schemas' });
describe('API Schema Tests', () => {
test('Users API schema has not drifted', async () => {
const res = await fetch('http://localhost:3000/api/users/1');
const data = await res.json();
const report = await guard.check('GET /users/:id', data);
expect(report.hasBreakingChanges).toBe(false);
expect(report.compatibilityScore).toBeGreaterThanOrEqual(90);
});
test('Orders API schema has not drifted', async () => {
const res = await fetch('http://localhost:3000/api/orders');
const data = await res.json();
const report = await guard.check('GET /orders', data);
expect(report.summary.breaking).toBe(0);
});
});Generate an HTML pass/fail test report:
npm run test:htmlThe report is written to:
test-reports/jest-report.html
from schema_sentinel import SchemaGuard
guard = SchemaGuard(store="./schemas")
def test_users_api_schema():
response = requests.get("https://api.example.com/v2/users/1")
report = guard.check("GET /users/:id", response.json())
assert report.breaking_changes == 0, f"API broke: {report.summary()}"import { SchemaGuard } from 'api-schema-differentiator';
import { expect } from 'chai';
const guard = new SchemaGuard({ store: './api-schemas' });
describe('API Contracts', () => {
it('should not have breaking schema changes', async () => {
const res = await fetch('http://localhost:3000/api/users');
const data = await res.json();
const report = await guard.check('GET /users', data);
expect(report.hasBreakingChanges).to.be.false;
});
});# .github/workflows/api-schema-check.yml
name: API Schema Drift Check
on: [push, pull_request]
jobs:
schema-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
# Run your API tests that generate response files
- run: npm test
# Check for schema drift
- run: npx api-schema-differentiator check --key "GET /api/users" --data test/responses/users.json --fail-on breaking
# Or check multiple endpoints
- run: |
npx api-schema-differentiator check -k "GET /api/users" -d test/responses/users.json --fail-on breaking
npx api-schema-differentiator check -k "GET /api/orders" -d test/responses/orders.json --fail-on breaking
npx api-schema-differentiator check -k "GET /api/products" -d test/responses/products.json --fail-on warning
# Generate a PR comment with the report
- run: npx api-schema-differentiator diff --before schemas/users/latest.json --after test/responses/users.json --format markdown > drift-report.mdschema-drift-check:
stage: test
script:
- npm install api-schema-differentiator
- npx api-schema-differentiator check -k "GET /api/users" -d responses/users.json --fail-on breaking
artifacts:
when: on_failure
paths:
- schemas/# Exit code 0 = no breaking drift, 1 = breaking drift detected
api-schema-differentiator check \
--key "GET /api/users" \
--data ./test-responses/users.json \
--store ./schemas \
--fail-on breaking
# Use --fail-on to control sensitivity:
# --fail-on breaking β only fail on breaking changes (default)
# --fail-on warning β fail on warnings too
# --fail-on info β fail on any change at allMonitor third-party or internal APIs you don't control. Schema Sentinel polls at a set interval and alerts you when the response shape changes.
# Basic: poll every hour
api-schema-differentiator watch --url "https://api.example.com/v2/users/1" --interval 1h
# With authentication
api-schema-differentiator watch \
--url "https://api.stripe.com/v1/charges" \
--header "Authorization: Bearer sk_test_abc123" \
--interval 6h
# With Slack notifications
api-schema-differentiator watch \
--url "https://partner-api.example.com/data" \
--interval 30m \
--alert-webhook "https://hooks.slack.com/services/T.../B.../xxx"
# POST request with body
api-schema-differentiator watch \
--url "https://api.example.com/graphql" \
--method POST \
--header "Content-Type: application/json" \
--body '{"query": "{ users { id name } }"}' \
--interval 1hInterval formats: 30s (seconds), 5m (minutes), 1h (hours)
| Format | Flag | Best For |
|---|---|---|
| Console | --format console |
Terminal output, local development |
| JSON | --format json |
CI/CD pipelines, machine processing |
| Markdown | --format markdown |
PR comments, documentation |
| HTML | --format html |
Standalone reports, email attachments |
| Change | Severity | Example |
|---|---|---|
| Field Added | info |
New field email appeared |
| Field Removed | breaking |
Field role no longer present |
| Type Changed | breaking |
id changed from number to string |
| Nullable Changed | warning |
name was non-null, now can be null |
| Array Items Changed | warning |
tags items changed from string to number |
| Nesting Changed | breaking |
role changed from string to object |
| Field Renamed | warning |
role removed, roles added (heuristic) |
| Format Changed | info |
created changed from ISO date to datetime |
| Required β Optional | warning |
Field email is no longer always present |
| Homogeneity Changed | warning |
Array went from all-same-type to mixed types |
Severity levels:
- π΄ Breaking β Will likely cause downstream failures
- π‘ Warning β Might cause issues, should investigate
- π’ Info β Safe additive changes, good to know about
Feed multiple API responses to teach Schema Sentinel which fields are always present vs sometimes present:
const guard = new SchemaGuard({ store: './schemas' });
// Response 1: full profile
await guard.learn('GET /users/:id', {
id: 1, name: 'Alice', email: 'alice@example.com', bio: 'Hello!'
});
// Response 2: minimal profile
await guard.learn('GET /users/:id', {
id: 2, name: 'Bob'
});
// Response 3: with nullable field
await guard.learn('GET /users/:id', {
id: 3, name: 'Charlie', email: null
});
// Now the schema understands:
// - id, name β required (always present)
// - email β optional, nullable
// - bio β optional| Format | Auto-Detected | Notes |
|---|---|---|
| JSON | β | Objects, arrays, nested structures |
| XML/SOAP | β | Converted to object then inferred |
| GraphQL | β | Extracts data field from { data, errors } responses |
Pass any of these as a string and Schema Sentinel will auto-detect the format:
// JSON string
await guard.check('my-api', '{"id": 1, "name": "Alice"}');
// XML string
await guard.check('soap-api', '<user><id>1</id><name>Alice</name></user>');| Option | Type | Default | Description |
|---|---|---|---|
store |
string | SchemaStore |
(required) | Path to store directory, or custom store instance |
autoSnapshot |
boolean |
true |
Auto-save schema on first check |
autoUpdate |
boolean |
false |
Auto-update schema when drift is detected |
minSeverity |
'info' | 'warning' | 'breaking' |
'info' |
Minimum severity to include in reports |
metadata |
Record<string, unknown> |
{} |
Custom metadata to save with snapshots |
Check a response against the stored schema. Auto-snapshots on first call.
Explicitly save a schema snapshot.
Feed a sample for multi-sample learning (merges with existing schema).
Compare two stored schema versions.
Compare two response objects directly (no store needed).
Format a report. Formats: 'console', 'json', 'markdown', 'html'.
List all monitored endpoint keys.
List all schema versions for a key.
api-schema-differentiator <command> [options]
Commands:
snapshot Save a schema snapshot from a response file
check Check a response against a stored snapshot
diff Compare two responses or schema versions
list List all monitored endpoints
history Show version history for an endpoint
watch Poll an endpoint and alert on drift
Global Options:
-s, --store <dir> Schema store directory (default: ./schemas)
-f, --format <fmt> Report format: console, json, markdown, html
-o, --output <file> Write report to a file
-h, --help Show help
-V, --version Show version
Schemas are stored as JSON files in a directory structure:
schemas/
GET__api__v2__users/
v1.json
v2.json
latest.json
POST__api__v2__orders/
v1.json
latest.json
Tip: Commit the schemas/ directory to Git to track API schema changes over time alongside your code.
Implement the SchemaStore interface for custom storage (database, S3, etc.):
import { SchemaStore, SchemaSnapshot } from 'api-schema-differentiator';
class MyCustomStore implements SchemaStore {
async save(snapshot: SchemaSnapshot): Promise<void> { /* ... */ }
async load(key: string): Promise<SchemaSnapshot | null> { /* ... */ }
async loadVersion(key: string, version: number): Promise<SchemaSnapshot | null> { /* ... */ }
async listVersions(key: string): Promise<SchemaSnapshot[]> { /* ... */ }
async listKeys(): Promise<string[]> { /* ... */ }
async delete(key: string): Promise<void> { /* ... */ }
}
const guard = new SchemaGuard({ store: new MyCustomStore() });MIT