How JSON Schema and TypeScript types catch different bugs
· 5 min read
JSON Schema vs TypeScript types: a compiled-and-crashed experiment (0 compile errors, 2 runtime TypeErrors), plus how modern stacks use both together.

- TypeScript types exist only at compile time; JSON Schema validates data at runtime
- We compiled a wrongly typed payload with zero complaints, then watched it crash in Node
- JSON Schema caught the same payload instantly with per-field error messages
- The two solve different halves of the problem; mature stacks use both deliberately
The JSON Schema vs TypeScript types debate ends once you see what each guard can know. TypeScript's handbook chapter on everyday types covers the static half; on the runtime half, Ajv can emit standalone compiled validators, and schema-first libraries like TypeBox generate one artifact that serves as both.
The core difference: compile time versus runtime
TypeScript types describe what your code believes about data while you write it. The compiler checks those beliefs across every call site, then erases them: the TypeScript handbook documents that types vanish entirely from emitted JavaScript. JSON Schema, specified at json-schema.org and introduced gently in its getting-started guide, is a validation document that runs against actual data whenever you invoke a validator.
That timing difference decides everything. Types protect the world you control: your functions calling each other with the shapes you promised. Schemas protect the world you do not: HTTP payloads, config files, queue messages, anything that crosses a trust boundary as bytes rather than as compiled code.
Proof by crash: the same payload through both guards
During research for this guide we ran a small but decisive experiment. A TypeScript file declared a User type, parsed hostile JSON from a string pretending to be a network response, asserted it with a cast, and called string methods on fields typed as strings, shown here alongside the payload that broke them:
type User = { id: number; email: string };const payload = JSON.parse('{"id": "abc", "email": 42}');const user = payload as unknown as User;console.log(user.email.trim());
The equivalent JSON Schema document, the one Python's jsonschema later enforced, declared "properties": {"id": {"type": "integer"}, "email": {"type": "string"}}, "required": ["id", "email"]. Environment details for anyone reproducing the run: tsc 5.3.3, Node v22.22.2, jsonschema 4.26.0.
tsc compiled this without a single warning. Running the output in Node produced:
TypeError: user.email.trim is not a function
The type system believed whatever the cast said and vanished at the moment of truth. We then validated the identical payload against an equivalent JSON Schema using Python's jsonschema library, which rejected it immediately with precise diagnostics:
ValidationError: 'abc' is not of type 'integer' (at id)ValidationError: 42 is not of type 'string' (at email)
Python's jsonschema library, version-pinned above, produced those diagnostics; any conforming validator returns the same verdict for this schema.
Same data, opposite outcomes. Neither tool failed; each was asked only what it can answer. The compiler checks beliefs between your own lines of code, where casts are trusted. The schema inspects values arriving from outside, where nothing deserves trust.
JSON Schema vs TypeScript types: coverage compared
| Concern | TypeScript types | JSON Schema |
|---|---|---|
| Catches typos inside your codebase | Yes, exhaustively | No |
| Validates incoming API payloads | Only if data was checked first | Yes, at runtime |
| Survives into production JavaScript | No, erased at build | Yes, plain data |
| Consumable by non-TypeScript services | No | Yes, language-neutral |
| Generates documentation or forms | Only through generated docs or codegen | Naturally |
| Refactoring support | Precise, IDE-driven | Manual schema edits |
Using both without duplication
Teams rarely choose one; they wire the two together so truth lives once. Schema-first stacks generate TypeScript types directly from JSON Schema, keeping the compile-time view derived from the runtime contract rather than drifting from it. Code-first stacks go the other way, emitting schemas from zod declarations, whose authors document exactly this pattern at zod.dev. Validators such as Ajv execute JSON Schema inside JavaScript at high speed, closing the runtime gap inside TS projects themselves.
The anti-pattern worth naming: hand-maintaining parallel definitions that slowly disagree. If your type says email is a string and your schema forgot to require it, the bug lives in the gap. Generation from one source of truth removes the gap class entirely; the compiler overview in the handbook explains what emission erases.
Where each belongs in a real stack
- Validate every untrusted input with JSON Schema at the boundary, before any cast exists
- Let TypeScript carry certainty inward from there, unchecked casts banned in review
- Keep schemas as published contracts for partner teams and public APIs, since the specification is deliberately implementation-neutral
- Preview and iterate on contracts with the JSON schema generator, and inspect payload shapes during debugging with the JSON formatter
- For the schema format itself beyond the comparison, our what-is-json-schema guide walks the keywords field by field using examples from the official getting-started guide
Two locks on two different doors
JSON Schema vs TypeScript types is a false rivalry once the experiment runs: one guards your code's internal consistency until build time, the other guards reality's bytes forever after. Compile-time confidence cannot inspect a network response, and a runtime validator cannot refactor your call sites. Put the schema at every boundary, let types rule everything behind it, and generate one from the other so the story never splits.
Related Tools

Written by
Chloe Bertrand
I hold one rule for everything on this beat: generated code is still code, and it lands in your repository where somebody has to read it. If you cannot show me what it produced, I cannot recommend the tool. Payload to typed client is the most useful of these. The generator reads a schema or a sample document, infers field types and emits a structure that fails at compile time when a field is missing. The limits are honest and worth stating. Types come from the samples provided, so a field seen only as null becomes nullable, and one seen as an integer may arrive as a string from a different endpoint. Generating from a real schema beats generating from a sample every time. SQL to ORM goes the other way and is harder to do well. The generator has to guess a data model from query text, and a join between two tables produces a nested structure whose shape depends on assumptions about cardinality. The generated layer is a reasonable starting point and rarely the finished article. Every generated file should carry a header saying so, should be excluded from formatting rules that fight it, and should never be edited by hand. Drift between the generator and a hand-edit is the failure mode that costs the most time, because the next regeneration removes the change silently. One security note belongs with the ORM pages. Query builders make the safe path easy, and raw fragments inside them put it back within reach. Every page covering generated query code shows which calls parameterise and which interpolate.