Python jsonschema: Validate Required Nested and Arrays
python jsonschema validate json required nested and arrays: Learn how to use Python jsonschema to validate JSON with required fields, nested objects, and arrays, inclu...
python jsonschema validate json required nested and arrays requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to enforce that a JSON document contains specific fields, nested objects, and arrays with certain item types, Python's jsonschema library provides a declarative way to express those rules. The core function validate() checks a JSON instance against a schema and raises a ValidationError if any constraint is violated. This article focuses on the concrete patterns for required properties, nested structures, and array items, so you can apply them directly to your own data validation tasks.
The Minimal Schema for Required Fields
The simplest use of jsonschema is to require that certain top-level keys exist. You define a schema as a Python dictionary, list the mandatory keys under required, and then pass both the instance and the schema to validate(). If a required key is missing, the library raises a ValidationError.
from jsonschema import validate, ValidationError schema = { "type": "object", "required": ["name", "email"], "properties": { "name": {"type": "string"}, "email": {"type": "string"} } } valid = {"name": "Alice", "email": "alice@example.com"} validate(instance=valid, schema=schema) # passes invalid = {"name": "Alice"} try: validate(instance=invalid, schema=schema) except ValidationError as e: print(e.message) # 'email' is a required property
The required list only applies to the current object level. It does not automatically make properties in nested objects required. Each nested object must have its own required list if you need that level of enforcement.
Validating Nested Objects with Their Own Required Fields
When a JSON document contains nested objects, you must define a schema for each nested level. The properties keyword maps a property name to its own schema, and that schema can include required for the nested object's fields. The validation recursively checks every level.
schema = { "type": "object", "required": ["user"], "properties": { "user": { "type": "object", "required": ["id", "profile"], "properties": { "id": {"type": "integer"}, "profile": { "type": "object", "required": ["first_name"], "properties": { "first_name": {"type": "string"}, "last_name": {"type": "string"} } } } } } } valid = {"user": {"id": 1, "profile": {"first_name": "Alice"}}} validate(instance=valid, schema=schema) # passes invalid = {"user": {"id": 1, "profile": {}}} try: validate(instance=invalid, schema=schema) except ValidationError as e: print(e.message) # 'first_name' is a required property
Notice that the error message points to the missing key inside the nested object. The jsonschema library tracks the full path, which you can access via e.path to know exactly where the failure occurred.
Enforcing Array Item Types and Required Fields in Arrays
Arrays add another layer: you must specify what each item looks like. Use the items keyword to define the schema for every element. If the array contains objects, you can apply the same required and properties rules inside items.
schema = { "type": "object", "required": ["orders"], "properties": { "orders": { "type": "array", "items": { "type": "object", "required": ["id", "total"], "properties": { "id": {"type": "integer"}, "total": {"type": "number"} } } } } } valid = {"orders": [{"id": 1, "total": 19.99}, {"id": 2, "total": 5.0}]} validate(instance=valid, schema=schema) # passes invalid = {"orders": [{"id": 1}]} try: validate(instance=invalid, schema=schema) except ValidationError as e: print(e.message) # 'total' is a required property
If you need to enforce a minimum or maximum length, add minItems and maxItems to the array schema. These work independently of the item schema and are checked before iterating over elements.
Controlling Extra Properties with additionalProperties
By default, jsonschema allows any property that is not listed in properties. This can hide typos or unexpected fields. To reject unknown keys, set additionalProperties to False. This is especially useful when you want to enforce a strict contract for incoming JSON.
schema = { "type": "object", "required": ["name"], "properties": { "name": {"type": "string"} }, "additionalProperties": False } valid = {"name": "Alice"} validate(instance=valid, schema=schema) # passes invalid = {"name": "Alice", "age": 30} try: validate(instance=invalid, schema=schema) except ValidationError as e: print(e.message) # Additional properties are not allowed ('age' was unexpected)
When additionalProperties is False, it applies to every level of the schema where it is defined. You can combine it with nested objects and arrays to create a fully closed data model.
Understanding ValidationError Messages and Debugging
The ValidationError object contains more than just message. The path attribute is a deque of keys and indices that lead to the failing part of the document. This is invaluable when you have deeply nested structures. You can also access validator and validator_value to see which keyword failed and its configured value.
from jsonschema import validate, ValidationError schema = { "type": "object", "required": ["data"], "properties": { "data": { "type": "object", "required": ["items"], "properties": { "items": { "type": "array", "items": {"type": "integer"} } } } } } instance = {"data": {"items": [1, "two", 3]}} try: validate(instance=instance, schema=schema) except ValidationError as e: print("Message:", e.message) print("Path:", list(e.path)) print("Validator:", e.validator)
This prints:
Message: 'two' is not of type 'integer'
Path: ['data', 'items', 1]
Validator: type
Knowing the path lets you log or return a precise error to the API consumer instead of a generic message. You can also use e.absolute_path and e.absolute_schema_path to see the location in the schema that failed.
Performance and Maintainability Considerations
For one-off validation, calling validate() is fine. But if you validate many documents against the same schema, the library re-parses the schema on every call. To avoid that overhead, compile the schema once using a validator class, such as Draft202012Validator, and reuse its validate method.
from jsonschema import Draft202012Validator schema = { "type": "object", "required": ["name"], "properties": { "name": {"type": "string"} } } validator = Draft202012Validator(schema) for doc in list_of_documents: validator.validate(doc) # reuses compiled schema
This reduces the overhead of schema parsing and can matter when processing thousands of requests. The compiled validator also exposes iter_errors(), which yields all errors instead of stopping at the first one. That can be useful for collecting multiple problems in one pass.
From a maintainability perspective, keep your schemas in separate modules or JSON files rather than inline dictionaries. This makes them easier to test and reuse across services. When you change a required field, the schema is the single source of truth, and the validation logic in your code does not need to change.
One common pitfall is forgetting that required only applies to the object where it is declared. A nested object's required fields must be specified in that nested schema. Similarly, array items are validated against the items schema, so any required fields inside those items must be listed there. Keeping this mental model in mind prevents surprising validation results when your JSON structure grows.