jsonschema: Validate JSON Data in Python (2026)
APIs, webhooks, config files and LLM tool calls all exchange JSON, and a single missing field or wrong type can crash code far away from where the bad data entered. JSON Schema is a language-neutral standard for describing what valid JSON looks like, and jsonschema is the reference Python implementation of it, with more than 470 million downloads a month on PyPI. This tutorial was tested with jsonschema 4.26.0 (plus referencing 0.37.0) on Python 3.13.5, and every output block below is the real output of the scripts shown.
Why jsonschema in 2026?
- One schema, many languages: the same JSON Schema file can be checked in Python, JavaScript, Go or Java, and shared with front-end teams.
- Full support for the current drafts, including Draft 2020-12, Draft 7 and Draft 4, through classes such as
Draft202012Validator. - It reports every problem in a document, with a JSON path to each one, instead of stopping at the first error.
- It works on plain dicts and lists, so there are no model classes to write. That makes it a good fit for data you do not own, such as third-party webhooks or LLM function-call arguments, which already come with a JSON Schema.
- Reusable schemas with
$ref, resolved through the modernreferencinglibrary.
If you control the data model inside your own app, class-based tools like Pydantic v2 or msgspec are often more convenient. jsonschema shines when the schema itself is the contract.
Install
pip install jsonschema
jsonschema 4.26 requires Python 3.10 or newer. Basic format checks such as email and ipv4 work out of the box; install jsonschema[format] if you need stricter checks for URIs, date-times and other formats that rely on extra packages.
Step 1: Validate a document and collect all errors
The schema below describes a user signup: required fields, an integer age range, a fixed list of plans, unique tags and additionalProperties: false to reject unknown keys. check_schema() first confirms that the schema itself is valid. validate() raises ValidationError on the first problem, while a validator object gives you is_valid() and iter_errors() to list everything that is wrong.
from importlib.metadata import version
from jsonschema import Draft202012Validator, FormatChecker, ValidationError, validate
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 2},
"email": {"type": "string", "format": "email"},
"age": {"type": "integer", "minimum": 18, "maximum": 120},
"plan": {"enum": ["free", "pro", "team"]},
"tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": True},
},
"required": ["name", "email", "plan"],
"additionalProperties": False,
}
print("jsonschema", version("jsonschema"))
Draft202012Validator.check_schema(schema) # the schema itself is valid
print("schema OK")
good = {"name": "Ayesha", "email": "ayesha@example.com", "age": 29, "plan": "pro", "tags": ["python"]}
validate(instance=good, schema=schema, format_checker=FormatChecker())
print("good user: valid")
bad = {"name": "A", "email": "not-an-email", "age": 15, "plan": "gold", "tags": ["x", "x"], "admin": True}
try:
validate(instance=bad, schema=schema, format_checker=FormatChecker())
except ValidationError as err:
print("first error:", err.message)
validator = Draft202012Validator(schema, format_checker=FormatChecker())
print("is_valid(bad):", validator.is_valid(bad))
errors = sorted(validator.iter_errors(bad), key=lambda e: e.json_path)
print(f"all {len(errors)} errors:")
for e in errors:
print(f" {e.json_path:<9} [{e.validator}] {e.message}")
Real output (basics)
Running python jsonschema_basics.py printed:
jsonschema 4.26.0
schema OK
good user: valid
first error: Additional properties are not allowed ('admin' was unexpected)
is_valid(bad): False
all 6 errors:
$ [additionalProperties] Additional properties are not allowed ('admin' was unexpected)
$.age [minimum] 15 is less than the minimum of 18
$.email [format] 'not-an-email' is not a 'email'
$.name [minLength] 'A' is too short
$.plan [enum] 'gold' is not one of ['free', 'pro', 'team']
$.tags [uniqueItems] ['x', 'x'] has non-unique elements
The good user passes silently. The bad document fails six rules at once and iter_errors() finds all of them. Each error has a json_path such as $.age that points to the bad value, a validator keyword that says which rule failed, and a human-readable message. That is exactly what you need to return a helpful 422 response from an API. Note that the email format is only enforced because a FormatChecker is passed in: by the JSON Schema specification, format is an annotation unless you ask for it to be checked.
Step 2: Reusable schemas with $ref and a batch report
Real projects share pieces such as an address between many schemas. Give each piece an $id, register it in a referencing.Registry, and point to it with $ref. The script below validates a batch of four orders, nested arrays included, and uses best_match() to choose the most relevant error for each failing order.
import json
from referencing import Registry
from referencing.jsonschema import DRAFT202012
from jsonschema import Draft202012Validator
from jsonschema.exceptions import best_match
address = {
"$id": "https://example.com/schemas/address.json",
"type": "object",
"properties": {
"city": {"type": "string"},
"zip": {"type": "string", "pattern": "^[0-9]{5}$"},
},
"required": ["city", "zip"],
}
order = {
"$id": "https://example.com/schemas/order.json",
"type": "object",
"properties": {
"id": {"type": "integer"},
"ship_to": {"$ref": "address.json"},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {"sku": {"type": "string"}, "qty": {"type": "integer", "minimum": 1}},
"required": ["sku", "qty"],
},
},
},
"required": ["id", "ship_to", "items"],
}
registry = Registry().with_resource(address["$id"], DRAFT202012.create_resource(address))
validator = Draft202012Validator(order, registry=registry)
payload = json.loads("""[
{"id": 1, "ship_to": {"city": "Lahore", "zip": "54000"}, "items": [{"sku": "PY-101", "qty": 2}]},
{"id": 2, "ship_to": {"city": "Karachi", "zip": "7500"}, "items": [{"sku": "PY-102", "qty": 1}]},
{"id": "3", "ship_to": {"city": "Islamabad", "zip": "44000"}, "items": []},
{"id": 4, "ship_to": {"zip": "10001"}, "items": [{"sku": "PY-103", "qty": 0}]}
]""")
ok = 0
for doc in payload:
errs = list(validator.iter_errors(doc))
if not errs:
ok += 1
print(f"order {doc['id']}: OK")
continue
top = best_match(errs)
print(f"order {doc['id']}: {len(errs)} error(s), best match at {top.json_path}: {top.message}")
print(f"valid {ok}/{len(payload)}")
Real output (batch)
order 1: OK
order 2: 1 error(s), best match at $.ship_to.zip: '7500' does not match '^[0-9]{5}$'
order 3: 2 error(s), best match at $.items: [] should be non-empty
order 4: 2 error(s), best match at $.ship_to: 'city' is a required property
valid 1/4
Order 1 is clean. Order 2 fails the five-digit ZIP pattern inside the referenced address schema, which proves that $ref resolution worked. Order 3 has two problems (a string id and an empty items list) and order 4 is missing the city and has a quantity of zero, yet the report stays short because best_match() picks one headline error while len(errs) still shows the full count. Swap the JSON string for a file loaded with orjson and the same loop validates large exports quickly.
Using jsonschema in an API or pipeline
Build the validator once at startup and reuse it; creating it on every request wastes time. A minimal helper for a web handler or data pipeline looks like this:
from jsonschema import Draft202012Validator, FormatChecker
SIGNUP = Draft202012Validator(schema, format_checker=FormatChecker())
def check_payload(data):
problems = [
{"path": err.json_path, "rule": err.validator, "message": err.message}
for err in SIGNUP.iter_errors(data)
]
return problems # an empty list means the payload is valid
Production tips
- Always declare
$schemaand use a specific validator class such asDraft202012Validator, so behaviour does not change when defaults change. - Call
check_schema()in your test suite so a typo in a schema is caught before it reaches production. - Pass a
FormatCheckerif you rely onformat; without it, emails and dates are not checked. - Use
additionalProperties: falsefor inputs you accept from outside, to catch misspelled keys early. - Do not fetch remote
$refURLs at runtime. Keep schemas in your repository and load them into aRegistry. - Return all errors to API clients, not only the first one; it saves them several round trips.
Conclusion
jsonschema lets you describe valid JSON once and enforce it anywhere in Python: validate a document, list every error with its exact path, enforce formats, and build reusable schemas with $ref and referencing. With jsonschema 4.26.0 on Python 3.13, a few lines of code turn untrusted JSON from APIs, files and LLMs into data you can rely on.