jsonschema validates Python dicts against JSON Schema. pip install jsonschema. Validate: from jsonschema import validate. validate(instance={"name":"Alice"}, schema={"type":"object","properties":{"name":{"type":"string"}}}). Invalid: raises ValidationError. Schema types: "string", "integer", "number", "boolean", "array", "object", "null". Multiple types: {"type":["string","null"]}. Required: {"required":["name","email"]}. Properties: {"properties":{"age":{"type":"integer","minimum":0}}}. Additional: {"additionalProperties":False} — reject unknown keys. {"additionalProperties":{"type":"string"}}. Pattern properties: {"patternProperties":{"^[A-Z]+$":{"type":"integer"}}}. String constraints: {"minLength":1,"maxLength":100,"pattern":"^\\d{5}$"}. Number: {"minimum":0,"maximum":100,"exclusiveMinimum":0,"multipleOf":5}. Array: {"type":"array","items":{"type":"string"},"minItems":1,"maxItems":10,"uniqueItems":True}. Enum: {"enum":["user","admin","mod"]}. Const: {"const":"active"}. Format: {"format":"email"} — requires jsonschema[format-nongpl]. "uri", "date-time", "date", "ipv4", "uuid". validate(data, schema, format_checker=FormatChecker()). anyOf: {"anyOf":[{"type":"string"},{"type":"integer"}]}. oneOf: exactly one. allOf: all must match. not: {"not":{"type":"null"}}. if-then-else: {"if":{"properties":{"role":{"const":"admin"}}},"then":{"required":["admin_key"]}}. $ref: {"$ref":"#/$defs/Address"}. $defs / definitions. Draft validator: from jsonschema import Draft7Validator, Draft202012Validator. v = Draft7Validator(schema). list(v.iter_errors(data)) — collect all errors. error.message, error.path, error.schema_path. Draft7Validator.check_schema(schema) — validate the schema itself. jsonschema.exceptions.best_match(errors) — pick most relevant error. RefResolver for external $ref URIs. Claude Code generates JSON Schema definitions, OpenAPI component schemas, and validation pipelines.
CLAUDE.md for jsonschema
## jsonschema Stack
- Version: jsonschema >= 4.21 | pip install "jsonschema[format-nongpl]"
- Draft: Draft7Validator(schema) | Draft202012Validator for latest features
- Validate: validate(data, schema, format_checker=FormatChecker()) — raises on first
- Bulk: list(validator.iter_errors(data)) — collects all ValidationErrors
- Error: exc.message | exc.path (deque of keys) | exc.schema_path
- Reuse: $defs + $ref — inline schema composition without duplication
- Format: {"format":"email"} — requires format_checker=FormatChecker() at call
jsonschema Validation Pipeline
# app/json_schemas.py — JSON Schema definitions and validation utilities
from __future__ import annotations
from collections import deque
from typing import Any, Iterator
from jsonschema import Draft7Validator, FormatChecker, ValidationError, validate
from jsonschema.exceptions import best_match
# ─────────────────────────────────────────────────────────────────────────────
# Shared schema fragments — referenced via $ref
# ─────────────────────────────────────────────────────────────────────────────
_DEFS = {
"Address": {
"type": "object",
"required": ["street", "city", "state", "postal_code"],
"additionalProperties": False,
"properties": {
"street": {"type": "string", "minLength": 1, "maxLength": 200},
"city": {"type": "string", "minLength": 1, "maxLength": 100},
"state": {"type": "string", "pattern": r"^[A-Z]{2}$"},
"postal_code": {"type": "string", "pattern": r"^\d{5}(-\d{4})?$"},
"country": {"type": "string", "minLength": 2, "maxLength": 2, "default": "US"},
},
},
"Money": {
"type": "object",
"required": ["amount", "currency"],
"additionalProperties": False,
"properties": {
"amount": {"type": "number", "exclusiveMinimum": 0},
"currency": {"type": "string", "minLength": 3, "maxLength": 3,
"pattern": r"^[A-Z]{3}$"},
},
},
"Pagination": {
"type": "object",
"properties": {
"page": {"type": "integer", "minimum": 1, "default": 1},
"page_size": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# User schemas
# ─────────────────────────────────────────────────────────────────────────────
CREATE_USER_SCHEMA: dict = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$defs": _DEFS,
"type": "object",
"required": ["email", "first_name", "last_name", "password"],
"additionalProperties": False,
"properties": {
"email": {
"type": "string",
"format": "email", # requires FormatChecker
"maxLength": 254,
},
"first_name": {"type": "string", "minLength": 1, "maxLength": 100},
"last_name": {"type": "string", "minLength": 1, "maxLength": 100},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 72,
},
"role": {
"type": "string",
"enum": ["user", "moderator", "admin"],
"default": "user",
},
"age": {
"type": ["integer", "null"],
"minimum": 0,
"maximum": 130,
},
"address": {
"oneOf": [
{"$ref": "#/$defs/Address"},
{"type": "null"},
],
},
"metadata": {
"type": "object",
"additionalProperties": {"type": "string"},
},
},
# Conditional — if admin, phone is required
"if": {"properties": {"role": {"const": "admin"}}},
"then": {"required": ["email"]}, # simplified: real rule would require phone
}
UPDATE_USER_SCHEMA: dict = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": False,
"minProperties": 1, # at least one field must be present
"properties": {
"first_name": {"type": "string", "minLength": 1, "maxLength": 100},
"last_name": {"type": "string", "minLength": 1, "maxLength": 100},
"role": {"type": "string", "enum": ["user", "moderator", "admin"]},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# Product schema
# ─────────────────────────────────────────────────────────────────────────────
PRODUCT_SCHEMA: dict = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$defs": _DEFS,
"type": "object",
"required": ["sku", "name", "price", "stock", "category"],
"additionalProperties": False,
"properties": {
"sku": {
"type": "string",
"pattern": r"^[A-Z0-9]+-\d{4,}$",
},
"name": {"type": "string", "minLength": 1, "maxLength": 200},
"price": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 100_000,
},
"stock": {"type": "integer", "minimum": 0},
"category": {
"type": "string",
"enum": ["Electronics", "Clothing", "Books", "Home", "Sports"],
},
"is_active": {"type": "boolean", "default": True},
"tags": {
"type": "array",
"items": {"type": "string", "minLength": 1},
"uniqueItems": True,
"default": [],
},
"weight_kg": {"type": ["number", "null"], "minimum": 0},
"price_obj": {"$ref": "#/$defs/Money"},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# Order schema
# ─────────────────────────────────────────────────────────────────────────────
ORDER_LINE_SCHEMA: dict = {
"type": "object",
"required": ["product_id", "sku", "quantity", "unit_price"],
"additionalProperties": False,
"properties": {
"product_id": {"type": "string", "minLength": 1},
"sku": {"type": "string", "pattern": r"^[A-Z0-9]+-\d{4,}$"},
"quantity": {"type": "integer", "minimum": 1, "maximum": 10_000},
"unit_price": {"type": "number", "exclusiveMinimum": 0},
},
}
CREATE_ORDER_SCHEMA: dict = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$defs": {**_DEFS, "OrderLine": ORDER_LINE_SCHEMA},
"type": "object",
"required": ["user_id", "lines"],
"additionalProperties": False,
"properties": {
"user_id": {"type": "string", "minLength": 1},
"lines": {
"type": "array",
"items": {"$ref": "#/$defs/OrderLine"},
"minItems": 1,
},
"notes": {
"type": ["string", "null"],
"maxLength": 500,
},
"shipping_address": {
"oneOf": [{"$ref": "#/$defs/Address"}, {"type": "null"}],
},
},
}
# ─────────────────────────────────────────────────────────────────────────────
# Validation utilities
# ─────────────────────────────────────────────────────────────────────────────
_FORMAT_CHECKER = FormatChecker()
def iter_errors(data: Any, schema: dict) -> Iterator[ValidationError]:
"""Yield all validation errors — does not stop at first failure."""
validator = Draft7Validator(schema, format_checker=_FORMAT_CHECKER)
yield from validator.iter_errors(data)
def collect_errors(data: Any, schema: dict) -> list[dict]:
"""Return all validation errors as serializable dicts."""
errors = []
for error in iter_errors(data, schema):
path = ".".join(str(p) for p in error.absolute_path) or "(root)"
errors.append({
"path": path,
"message": error.message,
"value": repr(error.instance)[:120],
})
return errors
def validate_or_raise(data: Any, schema: dict) -> None:
"""Raise ValidationError with the most relevant error on failure."""
validator = Draft7Validator(schema, format_checker=_FORMAT_CHECKER)
errors = list(validator.iter_errors(data))
if errors:
raise best_match(errors)
def is_valid(data: Any, schema: dict) -> bool:
"""Return True/False without raising."""
return Draft7Validator(schema, format_checker=_FORMAT_CHECKER).is_valid(data)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
# Valid user
new_user = {
"email": "[email protected]",
"first_name": "Alice",
"last_name": "Smith",
"password": "SecurePass1!",
"role": "admin",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "IL",
"postal_code": "62701",
},
}
errors = collect_errors(new_user, CREATE_USER_SCHEMA)
print(f"User errors: {errors}") # []
# Invalid user — multiple problems
bad_user = {
"email": "not-an-email", # format fail
"first_name": "", # minLength fail
"password": "short", # minLength fail
"role": "superuser", # enum fail
"extra_key": "oops", # additionalProperties fail
}
errors = collect_errors(bad_user, CREATE_USER_SCHEMA)
print(f"\n{len(errors)} validation errors:")
for e in errors:
print(f" [{e['path']}] {e['message']}")
# Product
product = {
"sku": "ELEC-1001",
"name": "Mechanical Keyboard",
"price": 89.99,
"stock": 50,
"category": "Electronics",
"tags": ["keyboard", "mechanical"],
}
print(f"\nProduct valid: {is_valid(product, PRODUCT_SCHEMA)}")
# Order with nested lines
order = {
"user_id": "usr-001",
"lines": [
{"product_id": "p1", "sku": "PROD-1001", "quantity": 2, "unit_price": 19.99},
{"product_id": "p2", "sku": "BOOK-2005", "quantity": 1, "unit_price": 39.99},
],
}
errors = collect_errors(order, CREATE_ORDER_SCHEMA)
print(f"Order errors: {errors}") # []
# best_match — single most relevant error
try:
validate_or_raise(bad_user, CREATE_USER_SCHEMA)
except ValidationError as exc:
print(f"\nbest_match error: {exc.message}")
print(f" path: {list(exc.absolute_path) or '(root)'}")
# Schema self-check
Draft7Validator.check_schema(CREATE_USER_SCHEMA)
print("\nSchema self-check passed.")
For the Cerberus alternative — Cerberus uses its own schema dict syntax ({"type":"string","minlength":1}) that is Python-specific and not portable to other languages, while jsonschema validates against the standard JSON Schema specification (Draft-07, Draft 2020-12) whose schemas can be embedded directly in OpenAPI specs and reused by JavaScript, Go, or Rust validators — one schema source of truth across the stack. For the Pydantic alternative — BaseModel validates at the Python object layer and cannot validate arbitrary dicts produced by json.loads() without first constructing the model, while Draft7Validator(schema).iter_errors(raw_dict) validates the raw parsed JSON in one call with no class definitions needed, making it the right choice for validating OpenAPI request/response payloads against the spec’s own schema objects or for building a generic validation middleware that selects the schema at runtime. The Claude Skills 360 bundle includes jsonschema skill sets covering Draft7Validator and Draft202012Validator, properties/required/additionalProperties, type/enum/const constraints, string pattern/format/minLength/maxLength, number minimum/maximum/exclusiveMinimum/multipleOf, array items/minItems/uniqueItems, anyOf/oneOf/allOf/not composition, if-then-else conditional validation, $defs and $ref for schema reuse, iter_errors for bulk collection, and best_match for user-friendly error reporting. Start with the free tier to try JSON Schema validation code generation.