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 validatorThis 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)
{}$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.
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 validatorStrings
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 validatoritems 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 validatorPut 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 validatoroneOf: exactly one must match
Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{ "type": "number" },
{ "type": "integer" }
]
}Data
55 is both a number and an integer, so it matches both options and fails.
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 validatorInclude 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 }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
{}"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"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.
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.