How to Validate JSON Schema: A Complete Developer Guide

DevToolkit Team · · 12 min read

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:

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:

{
  "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:

  1. Forgetting additionalProperties: false. By default, JSON Schema allows extra properties. If you want strict validation, set "additionalProperties": false. But be careful — this breaks allOf composition because each sub-schema doesn't know about the other's properties.
  2. Confusing required with type. A property being in required means it must exist. Its type says what value it must have. A required field with "type": "string" still rejects null — use "type": ["string", "null"] if null is valid.
  3. Not specifying $schema. Without the $schema keyword, validators may guess which draft to use, leading to inconsistent behavior across tools.
  4. Over-using pattern for formats. Instead of writing a regex for emails, use "format": "email". Built-in formats are maintained and tested. Use pattern for custom formats.
  5. Circular $ref without 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 a maxItems constraint.

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:

Performance Tips

If you're validating thousands of documents per second (API gateway, stream processing), keep these tips in mind:

Quick Reference: Most-Used Keywords

KeywordApplies toPurpose
typeAnyExpected data type
propertiesObjectDefine expected keys
requiredObjectMandatory keys
itemsArraySchema for elements
enumAnyAllowed values
constAnyExact value
minimum / maximumNumberRange constraints
minLength / maxLengthStringLength constraints
patternStringRegex match
formatStringSemantic type hint
$refAnyReference reusable schema
allOf / anyOf / oneOfAnySchema composition
if / then / elseAnyConditional validation
additionalPropertiesObjectAllow 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.

Enjoyed this article?

Get the free Developer Cheatsheet Pack + weekly tips on tools, workflows, and productivity.

Subscribe Free

Try These Tools

Related free tools mentioned in this article

Back to Blog