Comparison guide
JSON Schema vs TypeScript Types: When to Use Which
JSON Schema vs TypeScript types compared on runtime vs compile-time, portability, validation power, and ecosystem. With concrete examples and recommendations.
When a JSON payload crosses a network boundary, you have a choice: do you describe its shape with a JSON Schema (a JSON document) or with TypeScript types? Both look similar in editors, both promise type safety, both ship in millions of repos. They are not, however, the same thing — and choosing the wrong one is what makes the next API change painful.
Generate a JSON Schema from a sample or a TypeScript interface in your browser, or read on for a structural comparison and clear recommendations.
Quick answer
JSON Schema runs at runtime and is language-agnostic — every service that handles the payload can validate it. TypeScript types are compile-time only and language-specific — they catch bugs in your code, not in the data. Most production systems need both. If you can only pick one, pick the one that runs at the network boundary.
Comparison at a glance
| Criterion | JSON Schema | TypeScript types |
|---|---|---|
| When it runs | Runtime | Compile time only |
| Language | Any (Java, Go, Python, JS, …) | TypeScript / JavaScript only |
| Format | A JSON document | TypeScript source |
| Validates incoming data | Yes | No |
| Catches bugs in your code | No | Yes |
| Generates code | Yes (via codegen tools) | No |
| Cross-service contract | Yes | No |
| Editor autocomplete | Through codegen | Native |
| Used by | OpenAPI, JSON-RPC, Kafka schemas, Ajv | The TypeScript compiler |
What JSON Schema actually does
JSON Schema describes the shape of a JSON value as a JSON document. A validator reads the schema and the payload at runtime and returns either "valid" or a list of paths that failed.
Example schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "name", "qty"],
"properties": {
"id": { "type": "string", "pattern": "^widget-" },
"name": { "type": "string", "minLength": 1 },
"qty": { "type": "integer", "minimum": 0 }
},
"additionalProperties": false
}
Validating a payload
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = list(validator.iter_errors({"id": "widget-1", "name": "Widget", "qty": 5}))
The validator runs in Python, Go, JS, Rust — anywhere the standard has a library. If your system has a Python service writing to a Kafka topic that a Java service consumes, the same schema validates both ends.
Where JSON Schema shines
- Cross-language contracts. One schema, every service.
- Constraints beyond shape. Regex patterns, numeric ranges, enums, string formats — all enforceable at runtime.
- Self-describing payloads. Drop the schema next to the data; anyone can verify they match.
- First-class in OpenAPI 3.1. If you ship an OpenAPI spec, you already use JSON Schema.
Limits
- Verbose. Hand-writing a JSON Schema for a deeply nested payload is slow.
- No conditional logic that's portable.
if/thenexists but implementation support is uneven. - No editor autocompletion in your code unless you generate types from the schema.
What TypeScript types actually do
TypeScript types describe the shape of values in your code. They are compile-time annotations that the TypeScript compiler uses to catch bugs before the code runs. They vanish in the compiled JavaScript output.
type Widget = {
id: `widget-${string}`;
name: string;
qty: number;
};
function decrement(w: Widget): Widget {
return { ...w, qty: w.qty - 1 };
}
decrement({ id: 'widget-1', name: 'Widget', qty: 5 }); // ok
decrement({ id: 'widget-1', name: 'Widget' } as any); // compiles, fails at runtime
The compiler catches typos, missing fields, wrong shapes — but only inside code
it can see. The moment data comes from fetch, JSON.parse, localStorage, or
a message queue, TypeScript trusts you to have typed it correctly. If the API
ships a breaking change, the type stays a lie until you notice.
Where TypeScript types shine
- Editor autocomplete. Every IDE understands TypeScript natively.
- Cheap. No new tool, no new file format.
- Expressive. Template literal types, conditional types, mapped types — far beyond what JSON Schema can describe.
- One source of truth inside your app. Function signatures, props, return values — all checked.
Limits
- No runtime checks. Types are erased; bugs in incoming data slip straight through.
- TypeScript-only. A Go service can't read a TypeScript type.
- Brittle to API drift. Production bug, not compile error, when the API changes shape.
How to combine them in practice
The recommended setup is to keep one source of truth and derive the other.
Option A: schema first, types generated
Define the JSON Schema. Generate TypeScript types from it with
json-schema-to-typescript. Validate incoming data with Ajv (or equivalent),
then the typed value flows through your TypeScript code with confidence.
schema.json ─► src/types/widget.ts ─► used throughout the app
└► Ajv validator at the boundary
Best when multiple services share the payload and you need a portable contract.
Option B: Zod first, schema generated
Write a Zod schema in TypeScript. It validates at runtime and
z.infer derives the type. You can also emit a JSON Schema from the Zod schema
for cross-language consumers.
zod schema ─► inferred TS type ─► used throughout the app
└► zod-to-json-schema (when needed by non-TS consumers)
Best when the payload is TypeScript-only and you don't want a separate JSON file in the repo.
Option C: types only
Acceptable for internal payloads that never cross a network boundary — for
example, the shape of useReducer state. Use a TypeScript type and skip the
schema entirely.
When to use which
Use JSON Schema when
- The payload crosses language boundaries.
- You publish an API spec (OpenAPI, AsyncAPI, JSON-RPC schemas).
- You need to validate user-uploaded data, configuration files, or webhook payloads in production.
- You want a portable description of the contract that other teams can read.
Use TypeScript types when
- The values live entirely inside your TypeScript app — local state, function signatures, props.
- You're prototyping and don't need the validation overhead yet.
- The existing source of truth is a Zod schema and you're using its inferred types.
Use both when
- You ship a TypeScript service that consumes an external API. Validate with JSON Schema (or Zod) at the boundary, use the TypeScript types everywhere downstream.
Try both in the browser
Generate a JSON Schema from a sample or a matching TypeScript interface — both work on the same JSON payload, both run client-side, neither uploads your data. Validate any payload against any schema in the JSON Schema validator.
Closing recommendation
If your JSON ever comes from somewhere you don't control, you need JSON Schema (or an equivalent runtime validator like Zod) at the boundary — TypeScript alone cannot save you. If your data is internal-only, TypeScript types are usually enough. The mistake is using types where a schema is required, then being surprised when the API drifts.
Related guides
- JSON to TypeScript: Generate Interfaces
Generate accurate TypeScript interfaces from a JSON sample. Handles nested objects, unions, optional fields, and nullable values without hand-typing.
- JSON to Zod: Generate Runtime Schemas
Generate a Zod schema from a JSON sample for runtime validation, plus an inferred TypeScript type. Handles unions, nullables, and optional fields.