Drift detection
Detect silent contract changes as non-fatal findings by diffing the raw response against the validated value.
Wrap a stitch's output with drift when you want to catch an API quietly
changing shape and be notified through the event stream. Drift is schema-anchored
and diff-based: the output schema you already declare is the contract, and
drift computes the structural difference between the raw body and the validated
value to surface what changed.
Example
import { , } from 'stitchapi';
import { } from 'zod';
const = ({
: 'https://api.example.com',
: '/users/{id}',
: (.({ : .(), : .() })),
});On every call the engine validates the response against the schema, then diffs
the raw body against the validated value and emits each difference as a drift
event on the event stream.
Two tiers: validation and drift
These are distinct layers, not options on a dial.
Validation — the hard contract
Validation runs first. A missing required field or incompatible value
throws immediately with error code STITCH_DRIFT (a finding with
change: 'invalid', level: 'error'). On success the call returns the
validated value — defaults applied, types coerced, unknown keys stripped — so
the result matches the declared TypeScript type exactly.
Make a field required in the schema when its absence is a bug. That is all that is needed for a hard failure; no extra configuration is required.
Drift — the soft, non-fatal layer
When validation succeeds, drift diffs the raw body against the validated value. The delta is the drift:
| diff op | meaning | change | default level |
|---|---|---|---|
remove | a key the schema stripped | undeclared | info |
change | a value the schema coerced ("42" → 42) | coerced | warn |
create | a .default() fired because the field absent | defaulted | verbose |
Soft drift findings are always non-fatal. To make a change fatal, declare
the field required in the schema — it will surface as invalid / error from
validation, not drift.
Options
drift(schema, opts?) accepts an optional DriftOptions object with two keys:
ignore
import { , } from 'stitchapi';
import { } from 'zod';
const = ({
: 'https://api.example.com',
: '/users/{id}',
: (.({ : .(), : .() }), {
// Acknowledge known API surface we intentionally do not consume.
: ['meta', '_links'],
}),
});ignore: string | string[] suppresses soft drift findings whose path matches an
entry — a bare string is shorthand for a one-element list (ignore: 'meta' ≡
ignore: ['meta']). Use it for API fields you know about but have chosen not to
model — acknowledged but unconsumed surface. The grammar: nested keys join with
., array elements collapse to [] (so items[].meta suppresses every element's
meta), and a bare prefix (meta) suppresses that key and everything beneath it.
A stale entry in ignore is harmless — you keep ignoring a field that is still
present.
severity
severity controls how soft drift is leveled or filtered. Three shapes:
import { , } from 'stitchapi';
import { } from 'zod';
// 1. Single level string — surface only drift at this level (allowlist).
const = ({
: 'https://api.example.com',
: '/a',
: (.({ : .() }), {
: 'warn', // only coerced findings surface; undeclared (info) and defaulted (verbose) are dropped
}),
});
// 2. Array of levels — allowlist of multiple levels.
const = ({
: 'https://api.example.com',
: '/b',
: (.({ : .() }), {
: ['info', 'warn'], // drop verbose (defaulted), surface the rest
}),
});
// 3. Map — re-level a specific kind (all kinds still surface).
const = ({
: 'https://api.example.com',
: '/c',
: (.({ : .() }), {
: { : 'info', : 'info' },
}),
});Omit severity to surface all soft drift at its default level.
Variance is not drift
Because the schema is the contract, normal response variance never reads as
drift: an optional field that is absent, a string | null that is null, an
empty or heterogeneous array all satisfy a well-written schema, so nothing
is raised. Declare what may vary, and only genuine change is reported.
import { , } from 'stitchapi';
import { } from 'zod';
const = ({
: 'https://api.example.com',
: '/users/{id}',
: (
.({
: .(),
: .().(), // sometimes null — declared, so never drift
: .(.()), // may be empty — declared, so never drift
: .().(), // sometimes absent — declared, so never drift
}),
),
});The flip side: drift watches the surface you declare. What the provider changes in a field you do not model is, by definition, change you do not consume.
Detecting new fields
A brand-new field the schema does not declare shows up as an undeclared finding
(change: 'undeclared', default level info) — the schema strips the key, the
diff sees the removal, and drift reports it. No .strict() is needed: the diff
catches stripped keys automatically, and the output stays clean.
import { , } from 'stitchapi';
import { } from 'zod';
// A new field the API adds will show up as an `undeclared` finding at `info` level
// — no `.strict()` required, and the validated output is still clean.
const = ({
: 'https://api.example.com',
: '/users/{id}',
: (.({ : .(), : .() })),
});To suppress a known-but-not-consumed field, add it to ignore rather than
expanding the schema — that keeps the typed contract tight.
Findings arrive as drift events on the event
stream. Subscribe to inspect warn, info,
and verbose levels that never throw.
See Reference → Config types for the full
DriftOptions and DriftFinding tables.