Blog

How to validate JSON against a schema (JSON Schema guide)

The JSON Schema keywords you will use most, a complete API request example, validation in JavaScript and Python, and how to test schemas in the browser.

February 10, 2026

A mobile client starts sending "age": "thirty-two", and three services later something crashes with an error nobody can trace. JSON Schema lets you catch that at the front door, with a message that says exactly which field is wrong.

What is JSON Schema?

JSON Schema is a way to describe what valid JSON looks like: which fields must be present, what types they have, which values are allowed and how nested objects and arrays are shaped. It does for JSON data roughly what TypeScript types do for JavaScript, except the checks run while your program is running.

Schemas are written in JSON too, so people and programs can both read them. A validator takes a JSON document and a schema, then either says the document is valid or hands you a list of errors. You'll find it behind APIs, config files, forms and data pipelines.

The specification has gone through several versions, called drafts. Draft-07 is the most widely supported. Drafts 2019-09 and 2020-12 add features such as prefixItems, $defs and dynamic references, but draft-07 is still the safest choice if a schema has to work across many languages and tools.

Core schema keywords

type

type restricts a value to a data type. There are seven: string, number, integer, boolean, null, object and array. An array of types allows any of them.

Only strings
{ "type": "string" }
Only whole numbers
{ "type": "integer" }
A string or null
{ "type": ["string", "null"] }

properties and required

For objects, properties lists the expected keys and a schema for each value. required lists the keys that must be present. Anything not in required is optional, which surprises people: listing a property doesn't make it mandatory.

JSON
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 0 }
  },
  "required": ["name", "email"]
}

Here name and email are required strings and age is an optional non-negative integer. {"name": "Mei", "email": "mei@example.com"} is valid. {"name": "Mei"} fails because email is missing. And "age": "thirty-two" fails because it's a string, which is exactly the bug from the start of this post.

items for arrays

items gives the schema every array element must match. minItems and maxItems limit the length, and uniqueItems requires all elements to be different.

JSON
{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "id": { "type": "integer" },
      "tag": { "type": "string" }
    },
    "required": ["id", "tag"]
  },
  "minItems": 1,
  "uniqueItems": true
}

This accepts a non-empty array of tag objects, each with an integer id and a string tag, and no two elements identical.

enum and const

enum limits a value to a fixed list. const limits it to exactly one value. Both suit status fields and settings that must come from a known set.

JSON
{
  "type": "object",
  "properties": {
    "status": {
      "type": "string",
      "enum": ["pending", "active", "suspended", "deleted"]
    },
    "version": {
      "const": "1.0"
    }
  }
}

String keywords

  • minLength and maxLength limit the length.
  • pattern checks the string against a regular expression. The pattern is not anchored, so add ^ and $ to match the whole string.
  • format checks common formats such as email, uri, date-time, ipv4 and uuid. Many validators only enforce formats when you turn that on.
At least 8 characters, one capital letter and one digit
{
  "type": "string",
  "minLength": 8,
  "maxLength": 128,
  "pattern": "^(?=.*[A-Z])(?=.*[0-9])"
}

A complete example: validating an API request

Here's a schema for the body of a POST /api/users sign-up request. The server checks it before doing anything else, so bad input never reaches the database:

user-registration.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "UserRegistration",
  "description": "Body of the POST /api/users registration endpoint",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 3,
      "maxLength": 30,
      "pattern": "^[a-zA-Z0-9_-]+$",
      "description": "Letters, digits, underscores and hyphens"
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "password": {
      "type": "string",
      "minLength": 8,
      "maxLength": 128
    },
    "plan": {
      "type": "string",
      "enum": ["free", "pro", "enterprise"],
      "default": "free"
    },
    "tags": {
      "type": "array",
      "items": { "type": "string", "maxLength": 50 },
      "maxItems": 10,
      "uniqueItems": true,
      "default": []
    },
    "profile": {
      "type": "object",
      "properties": {
        "displayName": { "type": "string", "maxLength": 100 },
        "bio": { "type": "string", "maxLength": 500 },
        "website": { "type": "string", "format": "uri" }
      },
      "additionalProperties": false
    }
  },
  "required": ["username", "email", "password"],
  "additionalProperties": false
}

This document passes every constraint:

Valid
{
  "username": "alice_dev",
  "email": "alice@example.com",
  "password": "secureP4ss!",
  "plan": "pro",
  "tags": ["developer", "react"],
  "profile": {
    "displayName": "Alice",
    "bio": "Full-stack developer",
    "website": "https://alice.dev"
  }
}

This one manages to break six rules at once, which is impressive in its own way:

Invalid
{
  "username": "al",
  "email": "not-an-email",
  "password": "short",
  "plan": "premium",
  "tags": ["developer", "developer"],
  "extraField": true
}
Validation errors
(root): must NOT have additional properties
username: must NOT have fewer than 3 characters
email: must match format "email"
password: must NOT have fewer than 8 characters
plan: must be equal to one of the allowed values
tags: must NOT have duplicate items (items ## 1 and 0 are identical)

These are the messages the PulpMiner validator shows. Notice that the additionalProperties error is reported on the object that contains the extra key, which here is the top level, shown as (root).

Using JSON Schema in your application

Every major language has a JSON Schema library, so you can run the same schema in your API and your tests.

JavaScript and TypeScript with Ajv

Ajv compiles a schema into a validation function, which makes it one of the fastest validators for JavaScript. Format checks such as email come from the separate ajv-formats package.

JavaScript
import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // adds "email", "uri", "date-time" and more

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: "al", email: "bad" };

if (!validate(data)) {
  console.log(validate.errors);
  // [
  //   { keyword: "minLength", instancePath: "/username", ... },
  //   { keyword: "format", instancePath: "/email", ... }
  // ]
}

Without allErrors: true, Ajv stops at the first error, and your users fix their form one field at a time.

Python with jsonschema

jsonschema is the standard Python library. Heads up: it ignores format unless you pass a format checker:

Python
import jsonschema

schema = {
    "type": "object",
    "properties": {
        "username": {"type": "string", "minLength": 3},
        "email": {"type": "string", "format": "email"},
    },
    "required": ["username", "email"],
}

data = {"username": "al", "email": "bad"}

validator = jsonschema.Draft7Validator(
    schema, format_checker=jsonschema.FormatChecker()
)
for error in validator.iter_errors(data):
    print(f"{error.json_path}: {error.message}")

# $.username: 'al' is too short
# $.email: 'bad' is not a 'email'

Combining and reusing schemas

Once a schema grows past a few fields, you'll want to reuse pieces and express rules like “this or that”. These keywords do that:

  • allOf: the data must match every subschema. Use it to combine constraints or extend a base schema.
  • anyOf: the data must match at least one subschema, like a union type.
  • oneOf: the data must match exactly one subschema. Useful when variants must not overlap.
  • not: the data must not match the subschema.
  • if, then, else: apply different rules depending on whether the data matches a condition, for example requiring a five-digit ZIP code only when the country is "US".
  • $ref: points to another schema, so you can define a type once (in draft-07, usually under definitions) and reuse it.
  • additionalProperties: set to false to reject any key not listed in properties, which is what you usually want for API input.

This schema uses $ref for a shared address type, and if/then/else to require a billing address only when sameAsShipping is false:

JSON
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" }
      },
      "required": ["street", "city", "country"]
    }
  },
  "type": "object",
  "properties": {
    "shippingAddress": { "$ref": "#/definitions/address" },
    "billingAddress": { "$ref": "#/definitions/address" },
    "sameAsShipping": { "type": "boolean" }
  },
  "required": ["sameAsShipping"],
  "if": {
    "properties": { "sameAsShipping": { "const": false } }
  },
  "then": {
    "required": ["shippingAddress", "billingAddress"]
  },
  "else": {
    "required": ["shippingAddress"]
  }
}

sameAsShipping is required here on purpose. An if that only uses properties also passes when the key is missing. Without the required line, an object that left the field out would quietly get the then rules. It's the kind of bug that takes an afternoon to find.

How to use the JSON Schema validator

  1. Open the JSON Schema validator. Paste the document to check into JSON data on the left and your schema into JSON Schema on the right. To try it first, load one of the examples, such as Nested + $ref or oneOf / anyOf.
  2. Select Validate. If either box is not valid JSON, or the schema cannot be compiled, you see that error first.
  3. Read the result. A valid document gets a confirmation. Otherwise you get every error with its path in dot notation, such as home.zip, or (root) for the top level.
  4. Fix the schema or the data and validate again. Copy copies the results as plain text.

The validator uses Ajv with format checks turned on and supports draft-07. A schema whose $schema points to draft 2019-09 or 2020-12 won't compile, so remove that line or stick to draft-07 keywords. Validation runs in your browser.

Best practices

  • Start small. Begin with the required fields and add constraints as you need them. An overly strict schema breaks more often as the API changes.
  • Use additionalProperties: false for API input. It catches misspelled field names and keeps unexpected data out.
  • Describe your fields. A schema with description on each field doubles as documentation.
  • Reuse types with $ref. Define addresses, pagination parameters and error responses once.
  • Version your schemas. Keep them in version control next to the code and note which schema version matches which API version.
  • Test the edges. Try empty objects, null, empty arrays, very long strings and negative numbers. Schema bugs usually hide at the boundaries.

Need data from a webpage as JSON?

PulpMiner turns any public page into structured JSON and can save it as a live API. See it work on a demo page, no sign-up.

Try it on a demo page