jsonschema review
jsonschema 4.26.0 checks Python values against JSON Schema drafts 3, 4, 6, 7, 2019-09, and 2020-12. The one-shot `validate()` function raises on one failure; draft-specific validator objects can be reused, yield all failures, report paths into the instance and schema, and resolve `$ref` through a `referencing.Registry`. Validation does not turn numeric strings into numbers or insert defaults. Format keywords such as `email` remain annotations until a `FormatChecker` is supplied. Our Python 3.12 import finished in 0.34 seconds. Version 4.26.0's runtime change avoids importing `urllib.request` during validator import, while its documentation now covers the `uuid` format.
jsonschema 4.26.0 installed in 0.3 seconds, used 3 MB across 6 packages, imported in 0.34 seconds, and had 0 audit findings in our sandbox. Choose it when JSON Schema itself is the shared contract; choose a model library for coercive Python boundaries, and do not assume `format`, `default`, or remote references are automatic.
We installed it
| Install | ✓ · 0.3s | 6 packages on disk · 3 MB |
| Import | ✓ | import jsonschema in 0.34s · pure Python · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does jsonschema install cleanly?
Yes. In a fresh container with an empty cache, pip install jsonschema finished in 0.3s, leaving 6 packages and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does jsonschema need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import jsonschema succeeded in 0.34s.
jsonschema or fastjsonschema: which should you use?
fastjsonschema: Use it when one fixed schema dominates a measured hot path and generated validation code is acceptable. jsonschema 4.26.0 installed in 0.3 seconds, used 3 MB across 6 packages, imported in 0.34 seconds, and had 0 audit findings in our sandbox.
When should you not use jsonschema?
The boundary is owned entirely by typed Python models that should parse, coerce, and serialize values. Pydantic or msgspec combines those tasks with validation.
Discussed on
Use it if
- The schema is a language-neutral contract shared with JavaScript, APIs, editors, or configuration files.
- An application must select a JSON Schema draft explicitly and return precise paths for several validation failures.
- References should resolve from a controlled in-memory catalogue or retrieval function rather than ad hoc URL handling.
- Users or external systems supply the schema, so Python class definitions cannot be the only source of truth.
- The boundary is owned entirely by typed Python models that should parse, coerce, and serialize values. Pydantic or msgspec combines those tasks with validation.
- A measured hot path spends too much time interpreting one fixed schema. fastjsonschema generates Python code for repeated validation, with different extension and error-reporting tradeoffs.
- Email, URI, date, and UUID checks are expected merely because `format` appears in the schema. jsonschema requires optional dependencies where applicable and an explicit `FormatChecker`.
- The application expects `default` to fill absent fields or `type: integer` to convert `"3"`. JSON Schema treats those as annotation and validation concerns, not mutation rules.
- Existing integration is built around deprecated `RefResolver` callbacks and cannot migrate. Current reference examples use the separate `referencing` package and Registry resources.
- A library requires a `py.typed` marker from every typed dependency. The 4.26.0 distribution in our sandbox did not ship one.
Setup reality
We installed jsonschema 4.26.0 in a fresh Python 3.12 Bookworm sandbox in 0.3 seconds. The environment ended with 6 packages and 3 MB on disk. pip-audit found 0 known vulnerabilities. Distribution metadata lists 21 direct dependencies, Python 3.10 or newer, pure Python code, no py.typed marker, and an unknown package license value. import jsonschema succeeded in 0.34 seconds.
Choose the draft deliberately and call check_schema() during startup. The convenience validate() function first selects a validator from $schema, or uses the latest draft when that keyword is missing, and checks the schema before validating the instance. That behavior is useful for occasional input and repeated overhead in a record loop. Keep one Draft202012Validator or other draft object for a batch.
Version 4.26.0 has 2 named format extras: jsonschema[format] and jsonschema[format-nongpl]. They supply optional checker libraries, yet validation still needs format_checker=FormatChecker(). Availability varies by installed extras. Schema defaults also do nothing to the input. Code that extends a validator to mutate instances should isolate that behavior because validation order can make default insertion surprising inside nested or failing branches.
References now belong to referencing.Registry. Preload trusted Resource objects when possible. A retrieval callback may read files or make requests, so validate URI schemes, confine filesystem paths, set network timeouts, and avoid resolving untrusted $ref values with ambient access. iter_errors() is lazy, but anyOf, oneOf, and nested arrays can produce a large error tree. Sort by absolute_path and cap client responses while retaining full errors in internal diagnostics.
Patterns
Raise on an invalid value validate-one-instance
from jsonschema import validate, ValidationError
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {"price": {"type": "number"}},
"required": ["price"],
}
try:
validate({"price": "9.99"}, schema)
except ValidationError as error:
print(error.message)`validate()` raises one `ValidationError` and checks the schema first. It does not convert the string `"9.99"` to a number.
Check a schema once for a batch reuse-validator
from jsonschema import Draft202012Validator
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
valid_documents = [
document for document in documents
if validator.is_valid(document)
]The explicit class locks behavior to Draft 2020-12. Reusing the validator avoids selecting a draft and checking the same schema for every document.
Return stable paths for every failure collect-validation-errors
from jsonschema import Draft202012Validator
validator = Draft202012Validator(schema)
errors = sorted(
validator.iter_errors(instance),
key=lambda error: list(error.absolute_path),
)
problems = [
{
"path": list(error.absolute_path),
"message": error.message,
"validator": error.validator,
}
for error in errors[:50]
]`iter_errors()` is lazy and can produce many nested failures. Sorting makes output repeatable, and this example limits the client payload to 50 problems.
Choose one relevant nested failure select-best-error
from jsonschema import Draft202012Validator
from jsonschema.exceptions import best_match
error = best_match(
Draft202012Validator(schema).iter_errors(instance)
)
if error is not None:
raise ValueError(f"invalid payload at {list(error.absolute_path)}: {error.message}")`best_match()` applies a relevance heuristic. Keep the original error collection when a UI needs every field-level problem or branch context.
Reject an invalid formatted string activate-format-checks
# install with: pip install 'jsonschema[format]'
from jsonschema import Draft202012Validator, FormatChecker
validator = Draft202012Validator(
{"type": "string", "format": "email"},
format_checker=FormatChecker(),
)
validator.validate("person@example.com")The extra installs available format dependencies, and `FormatChecker()` activates them. Omitting either step can leave `format` as annotation only.
Resolve `$ref` from memory register-local-reference
from jsonschema import Draft202012Validator
from referencing import Registry, Resource
address = Resource.from_contents({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["city"],
"properties": {"city": {"type": "string"}},
})
registry = Registry().with_resource("urn:example:address", address)
root = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {"home": {"$ref": "urn:example:address"}},
}
validator = Draft202012Validator(root, registry=registry)A registered `Resource` resolves the URN without network or filesystem access. This is the current replacement for common `RefResolver` store patterns.
Map approved HTTPS references to local files restrict-reference-retrieval
import json
from pathlib import Path
from referencing import Registry, Resource
from referencing.exceptions import NoSuchResource
SCHEMA_ROOT = Path('/srv/app/schemas').resolve()
def retrieve(uri: str):
prefix = 'https://schemas.example.com/'
if not uri.startswith(prefix):
raise NoSuchResource(ref=uri)
path = (SCHEMA_ROOT / uri.removeprefix(prefix)).resolve()
if SCHEMA_ROOT not in path.parents:
raise NoSuchResource(ref=uri)
return Resource.from_contents(json.loads(path.read_text()))
registry = Registry(retrieve=retrieve)The callback rejects other origins and path escapes before reading. An unrestricted retriever can turn an untrusted `$ref` into local-file or network access.
Reuse one shape with `$defs` define-reusable-schema
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"money": {
"type": "number",
"minimum": 0,
}
},
"type": "object",
"properties": {
"subtotal": {"$ref": "#/$defs/money"},
"tax": {"$ref": "#/$defs/money"},
},
}Draft 2020-12 uses `$defs` for local reusable schemas. Older drafts may use `definitions`, so match the keyword to the declared `$schema`.
Reject unknown object properties validate-exact-object-shape
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0},
},
"required": ["name"],
"additionalProperties": False,
}Object schemas allow undeclared keys unless `additionalProperties` is false. `required` controls presence separately from allowed property names.
Require exactly one payload shape choose-one-schema-branch
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"oneOf": [
{
"type": "object",
"required": ["email"],
"properties": {"email": {"type": "string"}},
},
{
"type": "object",
"required": ["phone"],
"properties": {"phone": {"type": "string"}},
},
]
}`oneOf` requires exactly one branch to validate. Overlapping branches can make an apparently valid instance fail because two branches matched.
Teach a validator about a domain number customize-number-type
from decimal import Decimal
from jsonschema import Draft202012Validator, validators
def is_number(checker, instance):
return Draft202012Validator.TYPE_CHECKER.is_type(instance, 'number') or isinstance(instance, Decimal)
type_checker = Draft202012Validator.TYPE_CHECKER.redefine('number', is_number)
DecimalValidator = validators.extend(
Draft202012Validator,
type_checker=type_checker,
)
DecimalValidator({"type": "number"}).validate(Decimal('1.25'))A redefined type changes validation semantics for this validator class only. Keep such extensions named because they no longer match every implementation's default behavior.
Fail during startup on a broken schema inspect-schema-error
from jsonschema import Draft202012Validator, SchemaError
try:
Draft202012Validator.check_schema({
"type": 42,
})
except SchemaError as error:
raise RuntimeError(
f"invalid application schema at {list(error.absolute_path)}: {error.message}"
) from error`check_schema()` raises `SchemaError`, which is distinct from an instance `ValidationError`. Run it when loading application-owned schemas rather than on each request.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastjsonschema | PyPI | Use it when one fixed schema dominates a measured hot path and generated validation code is acceptable. |
| pydantic | PyPI | Use it for Python-owned models that need parsing, coercion, serialization, and type-hint integration. |
| msgspec | PyPI | Use it when typed Python structures and fast JSON or MessagePack decoding matter more than arbitrary JSON Schema input. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

