JSON Schema vs TypeScript: Runtime vs Compile-Time Validation
TypeScript catches type errors at compile time. JSON Schema validates data at runtime. They solve related but different problems, and most production applications eventually need both.
The core difference
| TypeScript | JSON Schema | |
|---|---|---|
| When it runs | Compile time (build step) | Runtime (your code calls a validator) |
| What it validates | Your code's type usage | External data (API bodies, user input) |
| Output | Build error or warning | Pass/fail + error messages |
| Language | TypeScript only | Any language |
| Validates external input | No — types are erased at runtime | Yes |
| Portable | No — TypeScript-specific | Yes — JSON, language-agnostic |
Why TypeScript alone is not enough for external data
TypeScript types are erased when the code compiles to JavaScript. At runtime, if an API returns an unexpected shape, TypeScript cannot catch it.
// TypeScript thinks this is safe at compile time
const res = await fetch('/api/user')
const user: User = await res.json() // No runtime check!
console.log(user.name.toUpperCase()) // Could throw if name is nullTypeScript gives you confidence about code you write. It cannot give you confidence about data that arrives at runtime from an external source.
What JSON Schema adds
JSON Schema validates the actual shape of a value at runtime. If an API response does not match the schema, validation fails with a descriptive error message before your code tries to use the data.
const userSchema = {
type: 'object',
required: ['id', 'name', 'email'],
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
additionalProperties: false,
}
// Validate before using
const valid = ajv.validate(userSchema, data)
if (!valid) throw new Error(ajv.errorsText())For schema examples across common patterns, see JSON Schema Examples.
Using both together
The most robust pattern is to define a JSON Schema for external data, validate at the boundary (API handler, form submit, file read), and then assert a TypeScript type after successful validation.
import Ajv from 'ajv'
const ajv = new Ajv()
interface User {
id: number
name: string
email: string
}
const userSchema = { /* ... */ }
const validateUser = ajv.compile<User>(userSchema)
function parseUser(raw: unknown): User {
if (!validateUser(raw)) {
throw new Error(ajv.errorsText(validateUser.errors))
}
return raw // TypeScript now knows this is User
}Libraries like zod and valibot unify both steps: you define a schema once and get both runtime validation and inferred TypeScript types.
When to reach for each
| Scenario | Use |
|---|---|
| Internal function signatures | TypeScript |
| Validating an API response | JSON Schema or zod |
| Validating user-submitted form data | JSON Schema or zod |
| Documenting an API contract (OpenAPI) | JSON Schema |
| Cross-language validation (Python, Go, Java) | JSON Schema |
| Database record types | TypeScript + ORM types |
Generating TypeScript types from JSON Schema
If you already have a JSON Schema, tools like json-schema-to-typescript can generate TypeScript interfaces from it, keeping your types and schema in sync.
Going the other direction — inferring a schema from a sample JSON payload — is useful when you have real data but no schema yet. See the JSON to TypeScript tool for instant interface generation from a sample object.
Validate a JSON Schema online
Test your schema against real data in Jsonic's JSON Schema Validator. Paste a schema and a payload and see validation errors highlighted immediately.
Open Schema Validator