Skip to content
LocalOnly
JSON Tools

How to Validate JSON Request Bodies with JSON Schema

Checking a request body against a JSON Schema replaces dozens of hand-written if statements. Here is a tested setup for Node and Python, and a 400 response that tells the client exactly what to fix.

Updated October 18, 20263 min readTool runs in your browser

Validating JSON with JSON Schema replaces dozens of hand-written checks. Every API endpoint that accepts JSON has to check it: required fields, types, allowed values, no unexpected keys. Writing those checks by hand is slow and they drift away from the documentation. A JSON Schema does the same job in one declaration that both the code and the docs can use.

The JSON Schema Validator checks a document against a schema in the browser, which is handy while writing the schema. This guide wires the same check into an API, in Node with AJV and in Python with the jsonschema package.

A schema for a new order

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["customerEmail", "items"],
  "additionalProperties": false,
  "properties": {
    "customerEmail": { "type": "string", "format": "email" },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["sku", "qty"],
        "additionalProperties": false,
        "properties": {
          "sku": { "type": "string", "pattern": "^[A-Z]-[0-9]+$" },
          "qty": { "type": "integer", "minimum": 1 }
        }
      }
    },
    "coupon": { "type": ["string", "null"], "maxLength": 20 }
  }
}

The examples below send this body, which breaks four rules at once:

{"customerEmail": "not-an-email", "items": [{"sku": "A-1", "qty": 0}, {"sku": "b7", "qty": 2}], "gift": true}

Validate JSON with AJV in Node

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

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const validateCreateOrder = ajv.compile(createOrderSchema);

function validateBody(validate) {
  return (req, res, next) => {
    if (validate(req.body)) return next();
    res.status(400).type("application/problem+json").json({
      type: "https://api.example.com/problems/invalid-body",
      title: "The request body is not valid.",
      status: 400,
      errors: validate.errors.map((error) => ({
        pointer: "#" + error.instancePath +
          (error.params.additionalProperty ? "/" + error.params.additionalProperty : ""),
        detail: error.message,
      })),
    });
  };
}

app.post("/orders", express.json(), validateBody(validateCreateOrder), createOrder);

Compile the schema once at startup, not on every request; compiling is the slow part. allErrors: true makes AJV report every problem instead of stopping at the first, so a client can fix everything in one round. For an extra property, AJV reports the path of the parent object, which is why the function appends the property name.

The response, 400 application/problem+json
{
  "type": "https://api.example.com/problems/invalid-body",
  "title": "The request body is not valid.",
  "status": 400,
  "errors": [
    { "pointer": "#/gift", "detail": "must NOT have additional properties" },
    { "pointer": "#/customerEmail", "detail": "must match format \"email\"" },
    { "pointer": "#/items/0/qty", "detail": "must be >= 1" },
    { "pointer": "#/items/1/sku", "detail": "must match pattern \"^[A-Z]-[0-9]+$\"" }
  ]
}

The response follows RFC 9457, Problem Details for HTTP APIs, which defines the type, title and status fields and shows a list of errors with JSON Pointers as an example extension. Clients can map each pointer straight to a form field.

Validate JSON with jsonschema in Python

from jsonschema import Draft7Validator, FormatChecker

validator = Draft7Validator(create_order_schema, format_checker=FormatChecker())

def validation_errors(body):
    return [
        {"path": error.json_path, "detail": error.message}
        for error in sorted(validator.iter_errors(body), key=lambda e: e.json_path)
    ]
validation_errors(body) for the same request
$                Additional properties are not allowed ('gift' was unexpected)
$.customerEmail  'not-an-email' is not a 'email'
$.items[0].qty   0 is less than the minimum of 1
$.items[1].sku   'b7' does not match '^[A-Z]-[0-9]+$'

Create the validator once, as with AJV. iter_errors returns every error, while validator.validate(body) raises only the first. Turn the list into the same problem details response in your web framework.

The format check that does nothing

In JSON Schema, format is an annotation by default, and each library decides whether to enforce it. Both setups above enforce it only because of one extra line:

  • In AJV, without addFormats(ajv), compiling the schema fails in strict mode with unknown format "email" ignored in schema. That error is helpful; turning strict mode off to silence it is not.
  • In Python, without format_checker=FormatChecker(), the email is not checked at all. The same request produced three errors instead of four, with no warning.

The checks are not equally strict either. jsonschema's email check only looks for an @, so x@ passes, while AJV rejects both x@ and a@b. If an exact rule matters, write it as a pattern, which every validator applies the same way.

Frequently asked questions

Command Palette

Search for a tool or command