Skip to main content
Reference · 9 min read

JSON Schema Explained, From Sample to Contract

What JSON Schema is for, the keywords that matter, how to evolve a schema without breaking clients, and what inference can and cannot give you.

JSON Schema is a vocabulary — written in JSON — for describing the shape of JSON data. It answers questions a type system cannot: not just “is this a string?” but “is it a string of at least eight characters matching this pattern, and is it required when type is email?”

It is used for API request validation, configuration validation, form generation, documentation, and test data generation. One artefact, several jobs.

A schema, annotated

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/user.json",
  "title": "User",
  "type": "object",
  "required": ["id", "email", "status"],
  "additionalProperties": false,
  "properties": {
    "id":     { "type": "integer", "minimum": 1 },
    "email":  { "type": "string", "format": "email", "maxLength": 254 },
    "status": { "enum": ["active", "invited", "disabled"] },
    "age":    { "type": "integer", "minimum": 18, "maximum": 130 },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "uniqueItems": true,
      "maxItems": 20
    },
    "profile": {
      "type": ["object", "null"],
      "properties": { "bio": { "type": "string", "maxLength": 500 } }
    }
  }
}
  • $schema declares which draft the document uses. Validators need it.
  • $id gives the schema a stable identity so other schemas can reference it with $ref.
  • required is a list of property names — it is *not* a flag on each property, which is the single most common mistake.
  • additionalProperties: false rejects anything not listed. Powerful, and a compatibility hazard: it means adding a field to your API is a breaking change for anyone validating with this schema.
  • enum is where a schema earns its keep. No inference tool can guess that status has exactly three legal values.

The keywords worth knowing

Composition

  • allOf — must satisfy every subschema. Used for “base object plus these extra fields”.
  • anyOf — must satisfy at least one.
  • oneOf — must satisfy exactly one. Use it for discriminated unions; error messages are worse than anyOf when it fails.
  • not — must not satisfy the subschema.
  • if / then / else — conditional requirements, such as “if type is card, cardNumber is required”.

Strings

minLength, maxLength, pattern (a regular expression), and format for date-time, date, email, uuid, uri, ipv4 and others. Note that format is an annotation by default — many validators do not enforce it unless you turn enforcement on. Ajv needs the ajv-formats package.

Numbers

minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf. Note that type: "integer" accepts 1.0, because that is mathematically an integer.

Generating a first draft

Writing a schema from scratch for an existing API is tedious. The schema generator infers one from a sample: types for every property, required fields derived from what is present across array items, merged item schemas, and detected string formats.

What inference cannot give you, and what you must add by hand:

  • enum values — a sample shows the values that occurred, not the values that are legal.
  • Ranges and lengths — that age must be at least 18 is a business rule, not a property of the data.
  • description for each field — the part future readers will actually use.
  • Conditional requirements between fields.
  • Whether additionalProperties should be closed.

Evolving a schema without breaking clients

The compatibility rules are the same as for any interface:

  • Safe: adding an optional property; widening a range; adding an enum value that consumers treat as unknown; relaxing a pattern.
  • Breaking: adding a required property; removing a property; narrowing a type; removing an enum value; setting additionalProperties: false on an existing schema.

Keep old versions of the schema in version control and run a structural diff between them before you publish. Type changes and new required fields are exactly what that diff surfaces.

Schema and types together

A schema validates at runtime; generated types check at compile time. They are complementary, and they should be generated from the same source of truth so they cannot drift. Generate both from the same sample document — the type generator and the schema generator use the same inference engine — and regenerate both when the API changes.