JSON Schema: What It Is and How to Use It

Learn JSON Schema from scratch. Understand types, constraints, composition keywords, $ref, and see a complete practical example for API validation.

JSONTech TeamFebruary 1, 202511 min read

What Is JSON Schema?

JSON Schema is a vocabulary that lets you describe the structure of JSON data. Think of it as a contract: it defines what fields a JSON document should have, what types those fields must be, and what constraints they must satisfy. When a document conforms to its schema, it is "valid." When it does not, a validator tells you exactly what went wrong.

This matters because JSON itself is structurally permissive. Nothing in the JSON specification prevents an API from returning a string where you expect a number, or omitting a required field. JSON Schema closes that gap.

Why JSON Schema Matters

  • API contracts. Define exactly what your API expects and returns. Both client and server teams can validate payloads independently.
  • Data validation. Catch malformed data at the boundary — in form submissions, webhook payloads, configuration files, or database writes.
  • Documentation. A schema is machine-readable documentation. Tools can generate human-readable docs, forms, and even mock data from it.
  • Code generation. Generate TypeScript interfaces, Go structs, or Python dataclasses directly from your schema.

The Basics: Type, Properties, Required

Every JSON Schema starts with type. This tells the validator what kind of value to expect at the top level:

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The user's full name"
    },
    "age": {
      "type": "integer",
      "description": "Age in years"
    }
  },
  "required": ["name"]
}

This schema says: "I expect a JSON object with a name property (string, required) and an optional age property (integer)." Any object missing name or providing a non-string name is invalid.

All JSON Schema Types

JSON Schema supports seven primitive types. Here is each one with a minimal schema example:

TypeValid ValuesSchema Example
string"hello", ""{"type": "string"}
number3.14, -1, 0{"type": "number"}
integer42, -7{"type": "integer"}
booleantrue, false{"type": "boolean"}
nullnull{"type": "null"}
object{"key": "value"}{"type": "object", "properties": {...}}
array[1, 2, 3]{"type": "array", "items": {...}}

Objects in Detail

Use properties to define expected fields, required to list mandatory ones, and additionalProperties to control whether extra fields are allowed:

{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["id", "email"],
  "additionalProperties": false
}

Setting additionalProperties: false rejects any object that contains keys not listed in properties. This is useful for strict API contracts.

Arrays in Detail

Use items to define what each element should look like, and minItems / maxItems to constrain length:

{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "maxItems": 10,
  "uniqueItems": true
}

This accepts an array of 1 to 10 unique strings. An empty array or an array with duplicates would fail validation.

Constraints and Validation Keywords

Beyond basic types, JSON Schema provides fine-grained constraints for each type:

String Constraints

KeywordDescriptionExample
minLengthMinimum number of characters"minLength": 1
maxLengthMaximum number of characters"maxLength": 255
patternRegex the string must match"pattern": "^[A-Z]{2}\\d{4}$"
formatSemantic format hint"format": "email"
enumAllowed values"enum": ["active", "inactive"]

Number Constraints

KeywordDescriptionExample
minimumValue must be >= this"minimum": 0
maximumValue must be <= this"maximum": 100
exclusiveMinimumValue must be > this"exclusiveMinimum": 0
exclusiveMaximumValue must be < this"exclusiveMaximum": 1000
multipleOfValue must be divisible by this"multipleOf": 0.01

Combining Schemas: allOf, anyOf, oneOf, not

Composition keywords let you build complex schemas from simpler ones:

  • allOf — The data must be valid against all listed schemas. Used to combine multiple constraints or extend a base schema.
  • anyOf — The data must be valid against at least one of the listed schemas. Useful for fields that accept multiple formats.
  • oneOf — The data must be valid against exactly one of the listed schemas. Good for discriminated unions.
  • not — The data must not be valid against the given schema.
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "type": { "const": "email" },
        "address": { "type": "string", "format": "email" }
      },
      "required": ["type", "address"]
    },
    {
      "type": "object",
      "properties": {
        "type": { "const": "phone" },
        "number": { "type": "string", "pattern": "^\\+?[0-9]{7,15}$" }
      },
      "required": ["type", "number"]
    }
  ]
}

This schema accepts either an email contact or a phone contact, but not both — a clean way to model tagged unions.

Reusability with $ref

Schemas get repetitive fast. The $ref keyword lets you reference a reusable definition:

{
  "type": "object",
  "properties": {
    "billing_address": { "$ref": "#/$defs/address" },
    "shipping_address": { "$ref": "#/$defs/address" }
  },
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "zip": { "type": "string", "pattern": "^[0-9]{5}(-[0-9]{4})?$" }
      },
      "required": ["street", "city", "zip"]
    }
  }
}

Both billing_address and shipping_address share the same structure without duplication. The $defs section (called definitions in older drafts) is the conventional place to store reusable schemas.

Complete Example: User Registration Validation

Here is a real-world schema for validating a user registration payload. It uses most of the features we have covered:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "UserRegistration",
  "description": "Schema for the POST /api/register endpoint",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 30,
      "pattern": "^[a-zA-Z0-9_]+$",
      "description": "Alphanumeric username, 3-30 characters"
    },
    "email": {
      "type": "string",
      "format": "email",
      "maxLength": 254
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128,
      "description": "At least 8 characters"
    },
    "age": {
      "type": "integer",
      "minimum": 13,
      "maximum": 150
    },
    "role": {
      "type": "string",
      "enum": ["user", "moderator"],
      "default": "user"
    },
    "acceptedTerms": {
      "type": "boolean",
      "const": true
    },
    "tags": {
      "type": "array",
      "items": { "type": "string", "maxLength": 20 },
      "maxItems": 5,
      "uniqueItems": true
    }
  },
  "required": ["username", "email", "password", "acceptedTerms"],
  "additionalProperties": false
}

Notice how each field has clear, enforceable constraints. A validator will reject a 2-character username, an age below 13, duplicate tags, or any extra fields not listed in the schema.

Try it yourself: Paste any JSON and generate a schema automatically with our JSON Schema Generator.

JSON Schema in the Real World

OpenAPI / Swagger

OpenAPI uses JSON Schema (with some extensions) to define request bodies, response shapes, and query parameters. Every schema block in an OpenAPI spec is a JSON Schema. If you write OpenAPI docs, you already write JSON Schema.

Form Validation

Libraries like react-jsonschema-form and ajv (the fastest JSON Schema validator for JavaScript) use schemas to generate and validate forms at runtime. Define your schema once, and both the backend and frontend can validate against the same rules.

Database Schemas

MongoDB supports JSON Schema validation at the collection level. You can set a $jsonSchema validator that rejects any document not conforming to your schema on insert or update.

Configuration Files

VS Code, ESLint, and many other tools use JSON Schema to validate their configuration files. That autocompletion you get when editing tsconfig.json? It is powered by JSON Schema.

Draft Versions Comparison

JSON Schema has evolved through several drafts. Here are the ones you will encounter:

DraftYearKey AdditionsStatus
Draft-042013Core vocabulary, $ref, basic typesLegacy (still widely used)
Draft-062017const, contains, propertyNamesLegacy
Draft-072018if/then/else, readOnly, writeOnlyWidely supported
2019-092019$defs, unevaluatedProperties, dependentRequiredSupported by major validators
2020-122020prefixItems (replaces tuple validation), dynamic $refCurrent / recommended

For new projects, use 2020-12. If you need maximum compatibility with existing tools, Draft-07 is the safe choice — it has near-universal library support.

Common Mistakes to Avoid

  • Forgetting required. Properties defined under properties are optional by default. If a field must be present, list it in required.
  • Confusing number and integer. number accepts decimals; integer does not. Use integer for IDs, counts, and other whole-number values.
  • Over-constraining too early. Start with the minimal schema that catches real errors. You can always tighten constraints later — loosening them is a breaking change.
  • Not using $ref. Duplicating the same sub-schema in multiple places is a maintenance nightmare. Extract shared structures into $defs.
  • Ignoring format validation. By default, most validators treat format as an annotation, not a constraint. You need to explicitly enable format validation (e.g., ajv.addFormat() or passing { validateFormats: true }).

Try it yourself: Generate a schema from any JSON sample with our JSON Schema Generator, then customize the constraints to match your requirements.

Related Tools