Skip to content
Free Tool Arena

Developers & Technical · Guide · Developer Utilities

How to write JSON schemas

Core JSON Schema keywords, composition (allOf, oneOf, $ref), draft differences, when to pick JSON Schema vs Zod or OpenAPI, runtime validation.

Updated April 2026 · 6 min read

JSON Schema is how you describe the shape of JSON data so machines can validate it, humans can understand it, and APIs can generate docs and clients from it. Write a schema once and you get validation, auto-completion, fixture generation, and contract tests for free. This guide covers the core keywords (type, properties, required, additionalProperties), the composition patterns (allOf, oneOf, $ref), how drafts differ, when to reach for JSON Schema vs alternatives (OpenAPI, Zod, TypeBox), and the common mistakes that let invalid data slip through.

Advertisement

A minimal schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age":  { "type": "integer", "minimum": 0 }
  },
  "required": ["name"]
}

This validates: {"name":"Alice","age":30}

Rejects: {"age":30} (missing name), {"name":"Alice","age":-1} (negative age).

Core types

string, number, integer, boolean, null, object, array. integer rejects 1.5 where number accepts it.

Union: "type": ["string", "null"] for fields that can be nullable.

String constraints

minLength / maxLength: character bounds.

pattern: regex the string must match.

format: email, uri, date-time, uuid, hostname. Formats are informational by default; enable format validation to enforce.

enum: whitelist of allowed values — "enum": ["draft","published","archived"].

Number constraints

minimum / maximum: inclusive bounds.

exclusiveMinimum / exclusiveMaximum: non-inclusive bounds.

multipleOf: divisor check. Useful for cents (multipleOf: 0.01) or step sizes.

Object constraints

properties: map of key → sub-schema.

required: array of keys that must be present. Absent in required means optional.

additionalProperties: controls unknown keys. false rejects any key not listed; a schema object validates extra keys against that schema.

patternProperties: match property names by regex, apply sub-schema to values.

minProperties / maxProperties: count bounds.

Array constraints

items: schema every element must match.

prefixItems (draft 2020-12): tuple mode — first element matches first schema, second matches second, etc.

minItems / maxItems: length bounds.

uniqueItems: enforce no duplicates.

contains: at least one item must match this schema.

Composition

allOf: value must match every sub-schema. Useful for extending a base schema.

oneOf: exactly one sub-schema must match. Common for discriminated unions.

anyOf: at least one sub-schema must match.

not: negation — value must NOT match the sub-schema.

$ref — reusable pieces

{
  "$defs": {
    "Address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city":   { "type": "string" }
      },
      "required": ["street","city"]
    }
  },
  "type": "object",
  "properties": {
    "shipping": { "$ref": "#/$defs/Address" },
    "billing":  { "$ref": "#/$defs/Address" }
  }
}

Definitions live in $defs; $ref points at them with a JSON Pointer.

Drafts matter

Draft 4: widely supported. Uses definitions, not $defs. Older API tooling often stuck here.

Draft 7: common default. Added if/then/else, readOnly, writeOnly.

2019-09: $defs replaces definitions. Recursive refs cleaned up.

2020-12: prefixItems for tuples. Cleaner array validation.

Rule: always declare $schema so validators pick the right version.

Conditional schemas — if/then/else

{
  "type": "object",
  "properties": { "kind": { "enum": ["user","admin"] }},
  "if": { "properties": { "kind": { "const": "admin" }}},
  "then": { "required": ["permissions"] }
}

If kind === "admin", permissions becomes required. Otherwise no extra constraint.

JSON Schema vs alternatives

OpenAPI: uses JSON Schema under the hood (with some extensions). Use OpenAPI for HTTP APIs where you want paths, responses, and auth all in one doc.

TypeScript interfaces: great for compile-time, useless at runtime. You need a runtime validator either way.

Zod / Yup / Joi (JS ecosystem): nicer DX, schema-as-code. Good for forms and internal validation. Export to JSON Schema when you need interop.

Protobuf / Avro: when you need binary efficiency and strict schema evolution rules, not JSON.

Tooling

Validators: ajv (JS, fastest), jsonschema (Python), go-jsonschema (Go).

Doc generators: json-schema-to-markdown, Redocly, Stoplight.

Code generators: json-schema-to-typescript, quicktype, datamodel-code-generator (Python Pydantic).

IDE integration: VS Code auto-validates JSON files against a matching schema. Huge productivity win for config files.

Common mistakes

Forgetting additionalProperties: false. Default is to allow unknown keys silently. Breaks contract testing.

Using type: number when you mean integer. Floats sneak through.

Missing required. Without required, every property is optional. Easy to miss on required fields.

Mixing drafts. $defs in draft 7 doesn’t work; definitions in 2020-12 doesn’t work (well, it’s allowed but unused).

Overly permissive enum. "enum": ["A", "a"] accepts both. Decide on case-sensitivity.

No format validation. format: email is a hint, not an enforced check unless you enable format mode in the validator.

Run the numbers

Generate a starter schema from sample JSON with the JSON schema generator. Pair with the JSON formatter to tidy your sample first, and the JSON to TypeScript converter to get types for your codebase.

Advertisement

Found this useful?Email