If you've ever built or consumed a REST API, you've dealt with JSON. But how do you make sure the JSON data your application receives actually matches what you expect? That's where JSON Schema comes in — a powerful, declarative way to describe and validate the structure of JSON data.
In this guide, we'll cover everything you need to know about JSON Schema validation: what it is, why it matters, how to write schemas from scratch, and how to validate them using code and online tools. Whether you're validating API responses, form submissions, or configuration files, this guide has you covered.
What Is JSON Schema?
JSON Schema is a vocabulary that lets you annotate and validate JSON documents. Think of it as a contract: it defines the expected structure, data types, constraints, and relationships within a JSON object. The schema itself is written in JSON, which makes it easy to read, share, and version-control.
The current stable specification is Draft 2020-12, though Draft 7 and Draft 2019-09 are still widely used. The core concepts are the same across drafts — only edge-case keywords differ.
Here's a minimal example. Say you expect a user object with a name (string) and age (integer):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name", "age"]
}
This schema says: "I expect a JSON object with a name property that is a string and an age property that is a non-negative integer. Both are required."
Why JSON Schema Validation Matters
Without schema validation, you're flying blind. Here's why it's worth the effort:
- Catch bad data early. Validate incoming API requests before they hit your business logic. A malformed payload caught at the validation layer is infinitely cheaper than a bug in production.
- Self-documenting APIs. A schema is both a validator and documentation. Tools like Swagger/OpenAPI use JSON Schema internally to describe request and response bodies.
- Contract testing. Share schemas between frontend and backend teams. If the schema passes, the data is guaranteed to be in the right shape.
- Config file safety. Validate CI/CD configs, Kubernetes manifests, or any YAML/JSON config before deployment.
- Code generation. Generate TypeScript interfaces, Go structs, or Python dataclasses directly from JSON Schema.
Core JSON Schema Keywords
Let's walk through the most important keywords you'll use in every schema.
type
The most fundamental keyword. Valid types are: string, number, integer, boolean, object, array, and null.
{ "type": "string" } // matches "hello", not 42
{ "type": "integer" } // matches 42, not 42.5
{ "type": ["string", "null"] } // matches "hello" or null properties and required
For objects, properties defines the expected keys and their schemas. required lists which properties must be present.
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"role": { "type": "string", "enum": ["admin", "user", "guest"] }
},
"required": ["email"]
}
Here, email is required, but role is optional. If role is present, it must be one of the three enum values.
items — Array Validation
For arrays, items defines the schema that each element must conform to.
{
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
This schema matches a non-empty array of unique strings, like ["apple", "banana"], but rejects [] (too few items) or ["a", "a"] (duplicates).
String Constraints
Strings support minLength, maxLength, pattern (regex), and format (semantic hints like email, date-time, uri).
{
"type": "string",
"minLength": 8,
"maxLength": 128,
"pattern": "^(?=.*[A-Z])(?=.*[0-9])"
} This could validate a password field: at least 8 characters, max 128, must contain at least one uppercase letter and one digit.
Number Constraints
Numbers support minimum, maximum, exclusiveMinimum, exclusiveMaximum, and multipleOf.
{
"type": "number",
"minimum": 0,
"maximum": 100,
"multipleOf": 0.01
} Perfect for a price field: non-negative, max 100, with two decimal places of precision.
Nested Objects and $ref
Real-world schemas are rarely flat. You'll often need nested objects and reusable definitions.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"$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"]
}
},
"properties": {
"name": { "type": "string" },
"billingAddress": { "$ref": "#/$defs/address" },
"shippingAddress": { "$ref": "#/$defs/address" }
},
"required": ["name", "billingAddress"]
}
The $ref keyword lets you point to a reusable definition. Here, both billingAddress and shippingAddress share the same address schema — no duplication. This is one of JSON Schema's most powerful features for keeping schemas DRY.
Composition: allOf, anyOf, oneOf
Sometimes you need more expressive schemas. JSON Schema provides three composition keywords:
allOf— must match ALL sub-schemas (intersection)anyOf— must match at least ONE sub-schema (union)oneOf— must match EXACTLY ONE sub-schema (exclusive or)
{
"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\\-\\s]+$" }
},
"required": ["type", "number"]
}
]
}
This schema validates a contact object that is either an email contact or a phone contact, but not both. The const keyword pins a property to a specific value, acting as a discriminator.
Conditional Schemas: if / then / else
Added in Draft 7, conditional schemas let you apply different validation rules based on the data:
{
"type": "object",
"properties": {
"country": { "type": "string" },
"postalCode": { "type": "string" }
},
"if": {
"properties": { "country": { "const": "US" } }
},
"then": {
"properties": { "postalCode": { "pattern": "^[0-9]{5}$" } }
},
"else": {
"properties": { "postalCode": { "pattern": "^[A-Z0-9\\s]+$" } }
}
}
If the country is "US", the postal code must be 5 digits. Otherwise, it accepts alphanumeric codes. This is far cleaner than trying to express conditional logic with oneOf.
Validating JSON Schema in Code
Now that you can write schemas, let's validate data against them in several popular languages.
JavaScript / Node.js (Ajv)
Ajv is the de facto standard JSON Schema validator for JavaScript. It's fast, spec-compliant, and supports all drafts.
import Ajv from 'ajv';
const ajv = new Ajv();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer', minimum: 0 }
},
required: ['name', 'age'],
additionalProperties: false
};
const validate = ajv.compile(schema);
const data = { name: 'Alice', age: 30 };
const valid = validate(data);
if (!valid) {
console.error(validate.errors);
} else {
console.log('Valid!');
} Python (jsonschema)
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "age"]
}
data = {"name": "Alice", "age": 30}
try:
validate(instance=data, schema=schema)
print("Valid!")
except ValidationError as e:
print(f"Invalid: {e.message}") Go (gojsonschema)
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func main() {
schema := gojsonschema.NewStringLoader(`{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "age"]
}`)
data := gojsonschema.NewStringLoader(`{"name": "Alice", "age": 30}`)
result, err := gojsonschema.Validate(schema, data)
if err != nil {
panic(err)
}
if result.Valid() {
fmt.Println("Valid!")
} else {
for _, err := range result.Errors() {
fmt.Println(err)
}
}
} Common JSON Schema Mistakes
Even experienced developers make these mistakes. Here are the most common pitfalls:
- Forgetting
additionalProperties: false. By default, JSON Schema allows extra properties. If you want strict validation, set"additionalProperties": false. But be careful — this breaksallOfcomposition because each sub-schema doesn't know about the other's properties. - Confusing
requiredwithtype. A property being inrequiredmeans it must exist. Itstypesays what value it must have. A required field with"type": "string"still rejectsnull— use"type": ["string", "null"]if null is valid. - Not specifying
$schema. Without the$schemakeyword, validators may guess which draft to use, leading to inconsistent behavior across tools. - Over-using
patternfor formats. Instead of writing a regex for emails, use"format": "email". Built-in formats are maintained and tested. Usepatternfor custom formats. - Circular
$refwithout guard. Recursive schemas (like a tree node referencing itself) are valid but can cause infinite loops in some validators. Always add a base case like"items": { "$ref": "#" }with amaxItemsconstraint.
Real-World Example: E-Commerce Order Schema
Let's put everything together with a realistic example — an e-commerce order:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"$defs": {
"lineItem": {
"type": "object",
"properties": {
"productId": { "type": "string", "format": "uuid" },
"name": { "type": "string", "minLength": 1 },
"quantity": { "type": "integer", "minimum": 1 },
"unitPrice": { "type": "number", "minimum": 0, "multipleOf": 0.01 }
},
"required": ["productId", "name", "quantity", "unitPrice"]
},
"address": {
"type": "object",
"properties": {
"line1": { "type": "string" },
"line2": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"zip": { "type": "string" },
"country": { "type": "string", "minLength": 2, "maxLength": 2 }
},
"required": ["line1", "city", "zip", "country"]
}
},
"properties": {
"orderId": { "type": "string", "format": "uuid" },
"customerId": { "type": "string", "format": "uuid" },
"items": {
"type": "array",
"items": { "$ref": "#/$defs/lineItem" },
"minItems": 1
},
"shippingAddress": { "$ref": "#/$defs/address" },
"status": {
"type": "string",
"enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
},
"createdAt": { "type": "string", "format": "date-time" }
},
"required": ["orderId", "customerId", "items", "shippingAddress", "status", "createdAt"]
}
This schema validates that every order has a UUID, at least one line item, a valid shipping address, and a known status. It uses $ref for reusable definitions, format for semantic types, and constraints like minItems and multipleOf for business rules.
Generate Schemas Automatically
Writing schemas by hand is educational, but for large payloads it's tedious. The fastest workflow is to paste a sample JSON document into a schema generator and then refine the output.
Try DevToolkit's free JSON Schema Generator — paste any JSON, get a valid schema instantly. Then tweak constraints, add required fields, tighten enum values, and you have a production-ready schema in minutes instead of hours.
This approach is especially useful for reverse-engineering schemas from existing API responses. Grab a real response, generate the schema, then lock it down.
Integrating JSON Schema in Your Pipeline
Validation isn't just for runtime. Here's how teams use JSON Schema across the development lifecycle:
- API middleware: Validate request bodies in Express/Fastify middleware before hitting controllers. Libraries like
ajvcompile schemas to optimized functions that validate in microseconds. - CI/CD gates: Validate config files (Kubernetes manifests, Terraform plans, GitHub Actions workflows) against schemas in your CI pipeline. Catch misconfigurations before deploy.
- OpenAPI / Swagger: Every OpenAPI spec uses JSON Schema to describe request/response bodies. Your schemas can be shared between docs and runtime validation.
- Database validation: MongoDB supports JSON Schema natively for collection-level validation. PostgreSQL has extensions for it. Validate at the data layer as a safety net.
- Frontend form validation: Libraries like
react-jsonschema-formgenerate entire forms from a schema, including validation, labels, and help text.
Performance Tips
If you're validating thousands of documents per second (API gateway, stream processing), keep these tips in mind:
- Pre-compile schemas. Ajv's
compile()returns a reusable function. Never re-parse the schema on every request. - Avoid
patternfor simple checks. Regex is slower than type/enum/const. Usepatternonly when the simpler keywords can't express the constraint. - Use
additionalProperties: falsesparingly. It forces the validator to check every key, not just the ones inproperties. - Benchmark your schemas. Ajv has a
--benchmarkmode. ComplexoneOfwith many branches can be surprisingly slow.
Quick Reference: Most-Used Keywords
| Keyword | Applies to | Purpose |
|---|---|---|
type | Any | Expected data type |
properties | Object | Define expected keys |
required | Object | Mandatory keys |
items | Array | Schema for elements |
enum | Any | Allowed values |
const | Any | Exact value |
minimum / maximum | Number | Range constraints |
minLength / maxLength | String | Length constraints |
pattern | String | Regex match |
format | String | Semantic type hint |
$ref | Any | Reference reusable schema |
allOf / anyOf / oneOf | Any | Schema composition |
if / then / else | Any | Conditional validation |
additionalProperties | Object | Allow or block extra keys |
Conclusion
JSON Schema is one of the most underused tools in a developer's toolkit. It catches bugs early, documents your APIs, enables code generation, and works across every language and platform. Start by generating a schema from sample data, then refine it with the keywords you learned here.
Ready to try it? Open DevToolkit's JSON Schema Generator — paste your JSON, get a schema in one click, then copy it into your project. No signup, no install, totally free.