Understanding JSON Schema: Validation, Types, and Best Practices

JSON is everywhere - API payloads, configuration files, database documents, message queues. But how do you ensure that the JSON your application receives actually matches the structure you expect? A missing field, a wrong type, or an unexpected null can cascade into bugs that are difficult to trace. That is where JSON Schema comes in.

JSON Schema is a declarative language for describing the structure, constraints, and semantics of JSON data. Think of it as a contract: it defines what valid data looks like, and validators enforce that contract automatically. This guide walks through JSON Schema from the ground up, covering every type, the most useful validation keywords, reusable definitions, and common mistakes. If you want to generate a schema from existing JSON data, try our JSON Schema Generator.

What Is JSON Schema?

JSON Schema is itself a JSON document that describes another JSON document. It defines expected types, required properties, value constraints (minimum, maximum, pattern), and structural rules (nesting, arrays, enums). Here is a minimal example:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "age": { "type": "integer", "minimum": 0 },
    "email": { "type": "string", "format": "email" }
  },
  "required": ["name", "email"]
}

This schema says: the data must be an object with a name (string, required), an age (integer, at least 0, optional), and an email (string in email format, required). Any JSON object that does not match this description is invalid.

The $schema keyword declares which version of the specification this schema follows. Using it is optional but recommended - it tells validators which features are available.

Basic Types and Their Validation Keywords

JSON Schema defines seven types. Each has type-specific keywords for constraining values.

string

Strings are the most common type. Key validation keywords:

  • minLength / maxLength - constrain the number of characters.
  • pattern - a regular expression the string must match. Example: "pattern": "^[A-Z]{2}\\d{4}$" requires two uppercase letters followed by four digits.
  • format - a semantic hint like "email", "uri", "date", "date-time", "uuid", or "ipv4". Note that format validation is optional by default in many validators - you may need to enable it explicitly.
  • enum - restricts the value to a fixed set: "enum": ["active", "inactive", "pending"].
{
  "type": "string",
  "minLength": 1,
  "maxLength": 255,
  "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}

number and integer

number allows any numeric value including decimals. integer restricts to whole numbers only. Key keywords:

  • minimum / maximum - inclusive bounds.
  • exclusiveMinimum / exclusiveMaximum - exclusive bounds.
  • multipleOf - value must be a multiple of this number. Useful for currency ("multipleOf": 0.01).
{
  "type": "number",
  "minimum": 0,
  "maximum": 100,
  "multipleOf": 0.01
}

boolean and null

boolean accepts only true or false. null accepts only null. These are simple types with no additional keywords. To allow a field to be either a string or null, use a type array: "type": ["string", "null"].

object

Objects are the workhorse of JSON Schema. Key keywords:

  • properties - defines schemas for each property by name.
  • required - an array of property names that must be present.
  • additionalProperties - controls whether properties not listed in properties are allowed. Set to false for strict validation or to a schema to constrain their type.
  • minProperties / maxProperties - constrain the number of properties.
  • patternProperties - define schemas for properties matching a regex pattern.
{
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string", "minLength": 1 },
    "tags": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["id", "name"],
  "additionalProperties": false
}

array

Arrays hold ordered lists of values. Key keywords:

  • items - a schema that every item in the array must match.
  • prefixItems (Draft 2020-12) - schemas for specific positions in a tuple. For example, [string, number] tuples.
  • minItems / maxItems - constrain array length.
  • uniqueItems - when true, all items must be distinct.
  • contains - the array must contain at least one item matching this schema.
{
  "type": "array",
  "items": { "type": "string", "minLength": 1 },
  "minItems": 1,
  "maxItems": 10,
  "uniqueItems": true
}

Nested Schemas and Composition

Real-world JSON is deeply nested. JSON Schema handles this naturally by nesting schema definitions:

{
  "type": "object",
  "properties": {
    "user": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "address": {
          "type": "object",
          "properties": {
            "street": { "type": "string" },
            "city": { "type": "string" },
            "zip": { "type": "string", "pattern": "^\\d{5}(-\\d{4})?$" }
          },
          "required": ["street", "city", "zip"]
        }
      },
      "required": ["name"]
    }
  }
}

For more complex scenarios, JSON Schema provides composition keywords:

  • allOf - data must match all of the listed schemas (intersection).
  • anyOf - data must match at least one schema (union).
  • oneOf - data must match exactly one schema (exclusive or).
  • not - data must not match the given schema.

These are powerful for modeling discriminated unions, polymorphic responses, and conditional validation.

Reusing Schemas with $ref and $defs

Duplicating schema definitions is a maintenance burden. The $ref keyword lets you reference a schema defined elsewhere. The convention is to define reusable schemas under $defs (called definitions in older drafts):

{
  "$defs": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "country": { "type": "string" }
      },
      "required": ["street", "city", "country"]
    }
  },
  "type": "object",
  "properties": {
    "billing": { "$ref": "#/$defs/address" },
    "shipping": { "$ref": "#/$defs/address" }
  }
}

Both billing and shipping reference the same address schema. Change it once, and both references update. $ref can also point to external files ("$ref": "address-schema.json") for cross-file reuse in larger projects.

Common Mistakes and How to Avoid Them

1. Forgetting the required Array

Listing a property in properties does not make it required. You must also add its name to the required array. Without it, the property is optional and validation will pass even if it is missing.

2. Confusing type: "number" with type: "integer"

"type": "number" accepts 3.14 and 42. "type": "integer" accepts only 42. If you need whole numbers (IDs, counts, indices), use integer. If you need decimals (prices, measurements), use number.

3. Assuming format Is Validated by Default

The format keyword ("format": "email", "format": "uri") is treated as an annotation by default in Draft 2019-09 and later. Many validators do not enforce it unless you explicitly enable format validation. Always check your validator's documentation.

4. Using additionalProperties: false Too Aggressively

Setting additionalProperties: false makes your schema strict, but it also makes it brittle. If the producer adds a new field, consumers with this setting will reject the data. Use it for internal schemas where you control both sides, but consider being lenient for public APIs.

5. Circular References Without a Base Case

Schemas can reference themselves for recursive data structures (like trees), but every recursive path must have a termination condition. Without it, validators will loop infinitely or crash.

Tools and Validators

Popular JSON Schema validators by language:

  • JavaScript/TypeScript: Ajv - the fastest JSON Schema validator for Node.js and browsers.
  • Python: jsonschema - the reference implementation for Python.
  • .NET: Newtonsoft.Json.Schema - reliable validation with detailed error messages.
  • Java: everit-org/json-schema - Draft 7 and 2020-12 support.
  • Go: santhosh-tekuri/jsonschema - high-performance validator.

For quick experimentation, our JSON Schema Generator creates a schema from any JSON you paste, and our JSON Validator checks data against a schema right in your browser.

Generate and Validate Your Schemas

Whether you are building a new API, documenting an existing one, or validating configuration files, JSON Schema gives you a machine-readable contract that catches errors before they reach production:

Frequently Asked Questions

JSON Schema is a vocabulary for annotating and validating JSON documents. It defines the expected structure, data types, required fields, value constraints, and patterns for JSON data. Common uses include validating API request and response payloads, enforcing configuration file formats, generating documentation and forms automatically, and ensuring data quality in pipelines.
JSON Schema defines seven primitive types: string, number (any numeric value including decimals), integer (whole numbers only), boolean (true or false), null, object (a collection of key-value pairs), and array (an ordered list of values). Each type has its own set of validation keywords. For example, strings support minLength, maxLength, and pattern, while numbers support minimum, maximum, and multipleOf.
The $ref keyword allows you to reference and reuse schema definitions instead of duplicating them. You define reusable schemas under a $defs (or definitions) section and reference them with $ref. For example, {"$ref": "#/$defs/address"} points to a schema defined at the $defs.address path within the same document. This keeps schemas DRY and maintainable. $ref can also point to external schema files via URLs.
When additionalProperties is set to false, the schema rejects any JSON object that contains properties not explicitly listed in the properties keyword. When set to true (the default), extra properties are allowed and pass validation. Setting it to false is stricter and catches typos in property names, but it makes schema evolution harder because adding new fields to the producer will break validation for consumers still using the old schema.
Use Draft 2020-12, the latest stable version. It introduces $defs as the replacement for definitions, adds prefixItems for tuple validation, and improves vocabulary support. Most modern validators support it. If you are working with an older system that only supports Draft 7 or Draft 4, use that version for compatibility, but prefer 2020-12 for new projects.