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.
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.
{
"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)
]$ 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 withunknown 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.
Related tools
Infer a JSON Schema from a sample JSON document.
Validate JSON syntax and pinpoint the exact line and column of any error.
Query JSON with JSONPath expressions and see live results.
Pretty-print JSON with configurable indentation and instant validation.
Related guides
A JSON Schema can generate the types your code uses, so the contract and the code cannot drift apart. Here is the output of two common generators, tested on one schema.
The same file opens cleanly in VS Code and is rejected by npm. The difference is which of three JSON dialects each tool reads.
Three query languages for JSON look alike and behave differently. Here are ten everyday queries in all three, with the results they actually return.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.