Release candidate — 1.0.0-rc.6
StitchAPI
GuidesValidation & drift

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 opmeaningchangedefault level
removea key the schema strippedundeclaredinfo
changea value the schema coerced ("42"42)coercedwarn
createa .default() fired because the field absentdefaultedverbose

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.

See also

On this page