JSON doesn't enforce anything on its own. You can send {"age": "twelve"} to an endpoint expecting a number and the format won't complain — catching that is on you. JSON Schema is the standard way to stop doing that in scattered application code. It's a document, written in JSON itself, that describes the exact shape another JSON object must have: which fields are required, what types they hold, what patterns strings must match. Define it once and any conforming validator handles the checking — in JavaScript, Python, Java, Go, and most other languages with a maintained implementation.
The current spec version is draft 2020-12, published as an IETF draft. The version matters because keyword behavior changed between drafts — more on that below.
The $schema and $id Keywords
Every schema should start with two meta-keywords. $schema declares which version of the spec the document follows. $id gives the schema a canonical URI — useful when schemas reference each other or are published for others to consume.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-registration.json",
"title": "User Registration",
"description": "Schema for a new user sign-up payload",
"type": "object"
}
Neither keyword is required for a schema to work locally, but including them is considered best practice and makes schemas self-documenting.
Core Keywords: type, properties, required, additionalProperties
type is the most fundamental constraint. Valid values are "string", "number", "integer", "boolean", "array", "object", and "null". You can also pass an array of types when a field may legitimately hold more than one, such as ["string", "null"] for a nullable field.
For objects, properties defines a schema for each named key, while required lists the keys that must be present. Note carefully: required only checks presence, not the value — a required field can still be null unless you also constrain its type. Setting additionalProperties to false rejects any key not listed under properties, useful for strict API contracts.
{
"type": "object",
"properties": {
"username": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["username", "email"],
"additionalProperties": false
}
String Constraints
minLength and maxLength set character-count bounds. pattern accepts a regular expression (ECMA 262 dialect) the string must match. enum restricts the value to a fixed list, and format declares a semantic format like "email", "uri", "date", or "uuid" — though format validation is optional in many implementations and must be explicitly enabled.
{
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"pattern": "^[a-zA-Z0-9_-]+$"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"]
}
}
Number Constraints
Numbers can be bounded with minimum, maximum, exclusiveMinimum, and exclusiveMaximum. multipleOf enforces divisibility — useful for things like page sizes that must be multiples of 10, or currency amounts that must be multiples of 0.01.
{
"age": {
"type": "integer",
"minimum": 13,
"maximum": 120
},
"score": {
"type": "number",
"minimum": 0,
"maximum": 100,
"multipleOf": 0.5
}
}
Array Validation
items provides a schema every element must satisfy. minItems and maxItems control array length, and uniqueItems: true requires no two elements be identical — handy for tag lists or permission sets.
{
"tags": {
"type": "array",
"items": { "type": "string", "minLength": 1, "maxLength": 50 },
"minItems": 1,
"maxItems": 10,
"uniqueItems": true
}
}
Nested Schemas with $ref and $defs
Real schemas quickly grow complex. $defs (called definitions in older drafts) lets you define reusable sub-schemas in one place and reference them with $ref — avoiding copy-pasting the same address schema into every object that has a billing or shipping address.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"country": { "type": "string", "minLength": 2, "maxLength": 2 }
},
"required": ["street", "city", "country"]
}
},
"type": "object",
"properties": {
"billingAddress": { "$ref": "#/$defs/address" },
"shippingAddress": { "$ref": "#/$defs/address" }
}
}
Complete Example: User Registration Payload
A realistic schema for a user registration API endpoint, combining type constraints, string validation, and required fields into a document you could drop into a validator today:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/register.json",
"title": "User Registration",
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"pattern": "^[a-zA-Z0-9_-]+$"
},
"email": {
"type": "string",
"format": "email",
"maxLength": 254
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 128
},
"age": {
"type": "integer",
"minimum": 13
},
"newsletter": {
"type": "boolean"
},
"interests": {
"type": "array",
"items": { "type": "string" },
"maxItems": 20,
"uniqueItems": true
}
},
"required": ["username", "email", "password"],
"additionalProperties": false
}
Validating with Ajv (JavaScript)
Ajv is the most widely used JSON Schema validator for JavaScript and Node.js. It compiles schemas to optimized validation functions, so repeated validations on the same schema are very fast. Install with npm install ajv:
import Ajv from "ajv";
import addFormats from "ajv-formats"; // for "email", "uri", etc.
const ajv = new Ajv();
addFormats(ajv);
const schema = {
type: "object",
properties: {
username: { type: "string", minLength: 3 },
email: { type: "string", format: "email" }
},
required: ["username", "email"]
};
const validate = ajv.compile(schema);
const data = { username: "alice", email: "alice@example.com" };
const valid = validate(data);
if (!valid) {
console.error(validate.errors);
} else {
console.log("Data is valid");
}
The validate.errors array contains structured error objects with a message, a JSON Pointer instancePath pointing to the failing field, and a keyword indicating which constraint was violated — making it straightforward to map errors back to form fields in a UI.
Validating with jsonschema (Python)
The jsonschema library is the standard choice in Python. Install with pip install jsonschema. The simplest usage calls validate(), which raises a ValidationError on failure:
import json
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"username": {"type": "string", "minLength": 3},
"email": {"type": "string", "format": "email"}
},
"required": ["username", "email"]
}
payload = {"username": "alice", "email": "alice@example.com"}
try:
validate(instance=payload, schema=schema)
print("Valid")
except ValidationError as e:
print(f"Invalid: {e.message}")
print(f"Field path: {list(e.absolute_path)}")
For validating many documents against the same schema, create a jsonschema.Draft202012Validator instance once and call its validate() method repeatedly to avoid re-parsing the schema on every call.
Testing Your Schemas
Before wiring a schema into application code, test it manually with sample payloads — both valid ones and deliberately broken ones that should fail. Paste your JSON into a formatter first to ensure it's well-formed before feeding it to a validator; catching a missing comma in your test data first saves confusion about whether the schema or the data is wrong.
JSON Schema is a powerful contract mechanism that moves validation logic out of imperative code and into a declarative, language-agnostic document. Once you have a schema, you can share it with API consumers, generate documentation from it, use it in CI to gate malformed requests, and even derive TypeScript types from it with tools like json-schema-to-typescript.
If you're testing a schema right now, JSON Formatter Hub formats and validates JSON entirely in your browser — a quick way to confirm your test payloads are well-formed before you throw them at a validator.
Top comments (0)