What is JSON Schema, and when do you actually need it?

JSON Schema is a JSON document that describes what another JSON document is allowed to look like, so a program can check new data against the contract automatically.

Published

You have a JSON payload — an API response, a config file, an event body — and the real problem is not reading it, it is trusting it. Something downstream will break the day a field arrives as a string instead of a number, or disappears entirely. JSON Schema is the standard answer: a JSON document that describes what another JSON document is allowed to look like, checkable by a program.

What a schema looks like

A schema for an object with a required id number and an optional email string:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": { "type": "number" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["id"]
}

Every keyword is a rule: type says what the value must be, properties names the allowed keys, required says which must be present. The "$schema" line pins the dialect — 2020-12 is the current one, and the keywords that exist depend on it.

What a schema can express

More than people expect, less than everything:

  • Types, and unions of types ("type": ["string", "null"] for an optional string)
  • Which properties are required, and whether unknown properties are rejected (additionalProperties: false — the difference between a strict contract and a loose description)
  • Number bounds (minimum, maximum), string length, array length, and array item rules (items)
  • Formats that read as annotations (date-time, email, uri) — validators check these only when told to
  • Recursive structure — a tree node containing tree nodes — via $ref
  • Conditional rules: if kind is "admin", then permissions is required

What it cannot do: enforce a value’s semantics. A schema can insist email is a string shaped like an address; it cannot insist the address belongs to a real person. A schema constrains shape, not meaning.

When you actually need one

Three situations, honestly:

  1. A public API. A schema is the machine-readable half of your documentation, and lets consumers validate before they file a bug that is really their own typo.
  2. Config files with teeth. A config validated against a schema fails at load, with the name of the offending field — not three layers later, mysteriously.
  3. Data you did not produce. Webhooks, third-party exports, files from a vendor. The schema is your tripwire.

If you control both ends and they sit in the same codebase, a schema is usually ceremony. TypeScript types do that job without another format.

Getting one from a sample

Writing a schema by hand is tedious, and the tedious parts are exactly what a generator does well: infer types from each value, merge the shapes of array items, notice which keys are always present. The schema generator on this site turns a real payload into a draft-2020-12 schema you can then tighten by hand — make fields required, reject unknown properties, add bounds — because the generator can only describe what it saw, and you know what the contract should say.

One honest caveat about generated schemas: a sample shows what the data did, not what it must do. If your sample happens to contain only administrators, the generated schema will not know that other roles exist. Generate the skeleton, then make the decisions.