DEV Community

楊東霖
楊東霖

Posted on • Originally published at devtoolkit.cc

How to Validate JSON Schema: A Complete Developer Guide

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"]
{'}'}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
{'}'}
Enter fullscreen mode Exit fullscreen mode

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
{'}'}
Enter fullscreen mode Exit fullscreen mode

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])"
{'}'}
Enter fullscreen mode Exit fullscreen mode

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
{'}'}
Enter fullscreen mode Exit fullscreen mode

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"]
{'}'}
Enter fullscreen mode Exit fullscreen mode

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"]
    {'}'}
  ]
{'}'}
Enter fullscreen mode Exit fullscreen mode

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]+$" {'}'} {'}'}
  {'}'}
{'}'}
Enter fullscreen mode Exit fullscreen mode

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!');
{'}'}
Enter fullscreen mode Exit fullscreen mode

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{'}'}")
Enter fullscreen mode Exit fullscreen mode

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)
        {'}'}
    {'}'}
{'}'}
Enter fullscreen mode Exit fullscreen mode

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 breaks allOf composition because each sub-schema doesn't know about the other's properties.

  • 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.

  • Not specifying $schema. Without the $schema keyword, validators may guess which draft to use, leading to inconsistent behavior across tools.

  • 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.

  • 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"]
{'}'}
Enter fullscreen mode Exit fullscreen mode

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 ajv compile 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-form generate 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 pattern for simple checks. Regex is slower than type/enum/const. Use pattern only when the simpler keywords can't express the constraint.
  • Use additionalProperties: false sparingly. It forces the validator to check every key, not just the ones in properties.
  • Benchmark your schemas. Ajv has a --benchmark mode. Complex oneOf with 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.

Top comments (0)