JSON Schema vs TypeScript Types: Which Guards What
Abhay Khant
Jan 1, 1970 • 4 min read
JSON Schema vs TypeScript Types: Which Guards What
- 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 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](https://www.typescriptlang.org/docs/handbook/intro.html) documents that types vanish entirely from emitted JavaScript. JSON Schema, specified at [json-schema.org](https://json-schema.org/specification) and introduced gently in its [getting-started guide](https://json-schema.org/learn/getting-started-step-by-step), 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:
const user = JSON.parse('{"id": "abc", "email": 42}') as unknown as User;
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)
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.
What each one covers
| 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 | Awkwardly | 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](https://zod.dev/). Validators such as [Ajv](https://ajv.js.org/) 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](https://www.typescriptlang.org/docs/handbook/intro.html) 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](https://json-schema.org/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](/blog/what-is-json-schema/) walks the keywords field by field using examples from the [official getting-started guide](https://json-schema.org/learn/getting-started-step-by-step)
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.


