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

TypeScriptJSON Schema
When it runsCompile time (build step)Runtime (your code calls a validator)
What it validatesYour code's type usageExternal data (API bodies, user input)
OutputBuild error or warningPass/fail + error messages
LanguageTypeScript onlyAny language
Validates external inputNo — types are erased at runtimeYes
PortableNo — TypeScript-specificYes — 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 null

TypeScript 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

ScenarioUse
Internal function signaturesTypeScript
Validating an API responseJSON Schema or zod
Validating user-submitted form dataJSON Schema or zod
Documenting an API contract (OpenAPI)JSON Schema
Cross-language validation (Python, Go, Java)JSON Schema
Database record typesTypeScript + 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