Skip to content
JSON Tidy
Learn JSON / JSON Schema

JSON Schema: how to describe and validate JSON

A JSON Schema is a JSON document that describes what other JSON should look like: which properties it needs, what type each one is, and which values are allowed. An API can use one to reject a bad request with a clear error before any of its own code runs.

Schemas also power autocomplete and error highlighting for config files in editors like VS Code, describe request and response bodies in OpenAPI, and generate types for TypeScript and other languages. This guide builds a schema for a sign-up request one step at a time, and every example can be opened in the JSON Schema validator.

Try the complete schema in the validator

This opens the finished sign-up schema with a request that has six mistakes for the validator to find. Open it with a correct request instead.

A first schema: types and properties

Start with the shape of the request: an object with a username and an age.

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "username": { "type": "string" },
    "age": { "type": "integer" }
  }
}

Data (passes)

{}
Open this example in the validator

$schema says which version of JSON Schema the file uses. type restricts the kind of value. The seven types are string, number, integer, boolean, object, array, and null. integer means a number with no fractional part, so 29 and 29.0 both count. properties gives a schema for each named property.

The empty object {} passes. properties only checks properties that are present; it doesn’t require any of them. That surprises almost everyone the first time, and it’s what the next keyword is for.

Required properties and extra properties

Schema

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

Data

{
  "username": "maya_chen",
  "emial": "maya@example.com"
}

This data fails twice: email is missing, and the misspelled emial isn’t allowed.

Open this example in the validator

required is a list of property names, and it goes on the object, next to properties. additionalProperties: false rejects any property that isn’t listed. That catches typos like emial. Without it, emial would be accepted as an extra property, and the only clue would be that email is missing. The trade-off is flexibility: if you add a field to your API later, clients running an older schema will reject responses that include it. Many teams use false for requests they receive and leave responses open.

Rules for values

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "username": { "type": "string", "minLength": 3, "maxLength": 20, "pattern": "^[a-z0-9_]+$" },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 13 },
    "plan": { "enum": ["free", "pro", "team"] }
  }
}

Data

{
  "username": "Maya Chen",
  "email": "maya@example",
  "age": 12,
  "plan": "premium"
}

All four values break a rule, and the validator reports each one.

Open this example in the validator

Strings

minLength and maxLength count characters. pattern is a regular expression in JavaScript syntax. format names a common kind of string, such as email, date-time, date, uri, or uuid. The specification lets validators treat format as a description only, so check that yours enforces it. Ajv needs the ajv-formats package, and Python’s jsonschema needs a FormatChecker. Even then, checks differ: Python’s email check only looks for an @, so it accepts maya@, which Ajv rejects.

Numbers

minimum and maximum include the limit itself; exclusiveMinimum and exclusiveMaximum don’t. multipleOf restricts values to steps, such as 5 for items sold in packs of five. Be careful with decimal steps. Because of floating-point rounding, Ajv rejects 19.99 as a multiple of 0.01 unless you set its multipleOfPrecision option. For money, it’s simpler to store whole cents as an integer, as the JSON format guide suggests.

Fixed values

enum lists every allowed value, and const allows exactly one. Both compare the whole value, including its type, so "1" doesn’t match 1.

Arrays

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "minItems": 1,
      "maxItems": 5,
      "uniqueItems": true
    },
    "point": {
      "type": "array",
      "prefixItems": [{ "type": "number" }, { "type": "number" }],
      "items": false
    }
  }
}

Data

{
  "tags": ["beta", "beta", 7],
  "point": [42.36, -71.06, 12]
}

The tags contain a duplicate and a number, and the point has a third value where only two are allowed.

Open this example in the validator

items is the schema every item must match, and minItems, maxItems, and uniqueItems limit the list as a whole. For a fixed-length list where each position means something different, such as a latitude and longitude pair, use prefixItems with one schema per position, and set items: false to forbid anything after them. Draft-07 wrote this as an array under items plus additionalItems.

Reusing definitions with $defs and $ref

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  },
  "$defs": {
    "address": {
      "type": "object",
      "required": ["street", "city", "country"],
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string", "minLength": 2, "maxLength": 2 }
      }
    }
  }
}

Data

{
  "billing": { "street": "1 Main St", "city": "Boston", "country": "US" },
  "shipping": { "street": "9 Elm St", "country": "USA" }
}

The shipping address is missing its city, and its country code is three letters instead of two.

Open this example in the validator

Put shared pieces under $defs and point to them with $ref. The value is a JSON Pointer into the schema, so #/$defs/address means “the address definition in this file”. Errors are reported at the place the data sits, here /shipping, so you can tell the two addresses apart. Older schemas use definitions instead of $defs; it works the same way. A $ref can also point to another file or URL, but that needs a validator that loads them. The JSON Tidy validator only follows references inside the schema.

Combining schemas: anyOf, oneOf, allOf, and if/then

anyOf: at least one must match

Schema

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

Data

{ "name": "Maya" }

A contact needs an email address or a phone number. This one has neither, so both options fail.

Open this example in the validator

oneOf: exactly one must match

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "oneOf": [
    { "type": "number" },
    { "type": "integer" }
  ]
}

Data

5

5 is both a number and an integer, so it matches both options and fails.

Open this example in the validator

This is the most common trap with oneOf. Use it only when the options can’t overlap, for example objects told apart by a "type" field with different const values. Otherwise use anyOf. allOf requires every listed schema to match, which is useful for combining a shared base with extra rules.

if, then, and else

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "country": { "type": "string" },
    "postalCode": { "type": "string" }
  },
  "if": {
    "properties": { "country": { "const": "US" } },
    "required": ["country"]
  },
  "then": {
    "properties": { "postalCode": { "pattern": "^[0-9]{5}$" } }
  }
}

Data

{ "country": "US", "postalCode": "SW1A 1AA" }

US postal codes must be five digits, and the rule only applies when the country is US.

Open this example in the validator

Include required in the if schema. Without it, an object with no country at all would match the condition, because properties ignores missing properties.

Allowing null

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "middleName": { "type": ["string", "null"] }
  }
}

Data (passes)

{ "middleName": null }
Open this example in the validator

To allow a value or null, list both types. OpenAPI 3.0 used nullable: true instead, which isn’t part of JSON Schema. OpenAPI 3.1 uses JSON Schema directly.

Validating in code

In JavaScript, Ajv is the most widely used validator. Its default import supports draft-07, so import the 2020-12 build for newer schemas, and add ajv-formats so that format is checked:

import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);

const validate = ajv.compile(schema);
if (!validate(data)) {
  console.log(validate.errors);
}

Compile each schema once and reuse the function, since compiling is the slow part. allErrors reports every problem instead of stopping at the first. In Python, use the jsonschema package:

from jsonschema import Draft202012Validator, FormatChecker

validator = Draft202012Validator(schema, format_checker=FormatChecker())
for error in validator.iter_errors(data):
    print(error.json_path, error.message)

Common mistakes

Putting required inside a property

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "email": { "type": "string", "required": true }
  }
}

Data

{}
Open this example in the validator

"required": true inside a property is the old draft-03 style. In current drafts, required must be a list on the parent object, so newer validators reject this schema, and older ones ignore it and require nothing.

Expecting pattern to match the whole string

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "string",
  "pattern": "[0-9]{5}"
}

Data (passes)

"Zip code: 02108, Boston"
Open this example in the validator

A pattern passes if it matches anywhere in the string. Wrap it in ^ and $ to match the whole value: "^[0-9]{5}$". See regular expressions in JSON Schema.

Using additionalProperties with allOf

Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "allOf": [
    { "properties": { "username": { "type": "string" } } }
  ],
  "properties": { "age": { "type": "integer" } },
  "additionalProperties": false
}

Data

{ "username": "maya_chen", "age": 29 }

username is defined, but it’s rejected anyway.

Open this example in the validator

additionalProperties only knows about the properties next to it, not those inside allOf or a $ref. In draft 2019-09 and later, use unevaluatedProperties: false, which accounts for every part of the schema. Open the fixed version.

Misspelled keywords

Validators ignore keywords they don’t recognize, so "minLenght": 3 does nothing and no error appears. Ajv’s strict mode, which is on by default in code, reports unknown keywords. Turn it on in tests even if you turn it off in production.

Which draft to use

Use draft 2020-12, the current version, for new schemas. Draft-07 is still common in older projects and tools. The differences that matter most are $defs instead of definitions, prefixItems for fixed-length arrays, and unevaluatedProperties. OpenAPI 3.1 uses draft 2020-12; OpenAPI 3.0 uses its own variant of an older draft.

Frequently asked questions

Is JSON Schema an official standard?

It’s published by the JSON Schema project at json-schema.org as a series of drafts. It isn’t an IETF RFC, but it’s the de facto standard: OpenAPI, many editors, and validators in nearly every language use it.

Can a schema compare two fields, like an end date after a start date?

Not directly. JSON Schema checks each value against fixed rules. It can require one field when another is present (dependentRequired) or apply rules conditionally (if/then), but comparing two values needs code.

What’s the difference between JSON Schema and a JSON validator?

A JSON validator checks syntax: quotes, commas, and brackets. A schema validator checks that already-valid JSON has the right shape and values. JSON Tidy has both: the JSON validator on the home page and the JSON Schema validator.

How do I write a schema for existing data?

Paste a sample into the JSON Schema validator and choose Generate schema from data. It records the types and which properties always appear. Then add the rules a sample can’t show, such as length limits and allowed values.