← Blog

September 24, 2026 · Steve Johnson

Two Schemas, Slowly Drifting Apart

An API I worked on described its request and response shapes in two separate places: a hand-written JSON Schema document for the public-facing specification, and a set of runtime validation objects that actually enforced those shapes when a request came in. They started out matching. By the time I looked closely, they didn't, in ways that only mattered the moment someone tried to build against the published spec and got a real request rejected for a reason the spec never mentioned.

Why two sources of truth drift, even with good intentions

Nobody sets out to let a hand-maintained schema fall out of sync with the code that actually validates against it. It happens one small change at a time: a field gets an extra constraint added to the runtime validator during a bug fix, and updating the separately maintained specification document is a second, easy-to-forget step that isn't enforced by anything. Do that enough times, across enough small fixes, and the two documents that are supposed to describe the same contract quietly stop agreeing — with nothing in the toolchain positioned to notice, because they're two independent artifacts with no dependency between them.

The fix wasn't better discipline

The fix wasn't "try harder to remember to update both." It was collapsing two sources of truth into one: define the shape once, as the runtime validation schema, and generate everything else — the machine-readable JSON Schema for tool integration, the human-facing API specification — directly from that single definition at request time. There is no longer a second document to forget to update, because there is no longer a second document.

Why the generation format choice mattered too

The generated specification format mattered for a reason that's easy to miss: picking a specification version whose schema representation is itself, natively, the same schema language the validation library already emits meant the generation step could be a direct pass-through instead of a lossy translation layer that would have reintroduced its own drift risk. Collapsing to one source of truth only actually holds if the generation path from that source doesn't itself become a second place for the two descriptions to quietly diverge again.