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.
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:
| Type | Valid Values | Schema Example |
|---|---|---|
string | "hello", "" | {"type": "string"} |
number | 3.14, -1, 0 | {"type": "number"} |
integer | 42, -7 | {"type": "integer"} |
boolean | true, false | {"type": "boolean"} |
null | null | {"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
| Keyword | Description | Example |
|---|---|---|
minLength | Minimum number of characters | "minLength": 1 |
maxLength | Maximum number of characters | "maxLength": 255 |
pattern | Regex the string must match | "pattern": "^[A-Z]{2}\\d{4}$" |
format | Semantic format hint | "format": "email" |
enum | Allowed values | "enum": ["active", "inactive"] |
Number Constraints
| Keyword | Description | Example |
|---|---|---|
minimum | Value must be >= this | "minimum": 0 |
maximum | Value must be <= this | "maximum": 100 |
exclusiveMinimum | Value must be > this | "exclusiveMinimum": 0 |
exclusiveMaximum | Value must be < this | "exclusiveMaximum": 1000 |
multipleOf | Value 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:
| Draft | Year | Key Additions | Status |
|---|---|---|---|
| Draft-04 | 2013 | Core vocabulary, $ref, basic types | Legacy (still widely used) |
| Draft-06 | 2017 | const, contains, propertyNames | Legacy |
| Draft-07 | 2018 | if/then/else, readOnly, writeOnly | Widely supported |
| 2019-09 | 2019 | $defs, unevaluatedProperties, dependentRequired | Supported by major validators |
| 2020-12 | 2020 | prefixItems (replaces tuple validation), dynamic $ref | Current / 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 underpropertiesare optional by default. If a field must be present, list it inrequired. - Confusing
numberandinteger.numberaccepts decimals;integerdoes not. Useintegerfor 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
formatvalidation. By default, most validators treatformatas 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.