Skip to content
Guides

4 JSON Schema Recursive Tree Tests Before API Release

·9 min read
4 JSON Schema Recursive Tree Tests Before API Release - Guides guide from JSON Prettifier

Quick answer: Test recursive JSON trees at four boundaries: finite leaf termination, repeated node constraints, whole-tree resource budgets, and packaged schema execution. These examples use a static self-reference in Draft 2020-12. Supporting recursion does not automatically require children, close descendant objects, limit total depth, or guarantee that a deployed consumer loads the same contract.

A menu’s root validates, but a deeply nested item has the wrong name type. Another menu has valid nodes yet overwhelms a handler that recursively renders every branch. The structural contract and the application’s resource policy need different tests.

The schema reference checks cover URI targets, registries, and packaging more broadly. Here the focus is the submitted tree and its acceptance boundary. Wikipedia’s tree overview explains the data structure; the examples below remain ordinary finite JSON objects and arrays.

1. Does a finite leaf terminate the recursive structure deliberately?

Define a concrete leaf representation and test it before adding nested examples. A recursive reference reuses a node contract but does not require an infinite instance. Decide whether children must exist, may be omitted, or may be null, then test those cases separately so the application never guesses how a valid branch ends during traversal.

{
  "$schema":"https://json-schema.org/draft/2020-12/schema",
  "$id":"urn:example:release-tree",
  "type":"object",
  "properties":{
    "name":{"type":"string","minLength":1},
    "children":{"type":"array","items":{"$ref":"#"}}
  },
  "required":["name","children"],
  "additionalProperties":false
}

The leaf {"name":"Home","children":[]} passes this contract. An object without children fails, and an explicit null children value fails the array type. An empty name fails too. This is one deliberate API design: every node carries a children array, and the empty array identifies a leaf without a special sentinel value.

The official schema-structuring guide describes recursive references. In this example, the root fragment refers to the same node contract when evaluating each array element. That is schema reuse through reference strings, not a JSON document that points back to an earlier object instance.

Keep the leaf decision visible in generated client models and handler code. A handler written to iterate children directly can fail if another schema variant permits omission or null. If the product wants one of those variants, change the contract and fixtures together rather than adding an undocumented fallback in one consumer. Acceptance tests should describe what reaches the handler, not merely the values a demonstration happens to send.

Distinguish valid recursive structure from an accidental schema loop that never advances through instance data. A schema referring to itself through an unconditional composition branch is not the same pattern as a child item referring to its node definition. Do not experiment with such loops inside the ordinary release test process. Keep schema sources trusted and investigate potentially nonterminating cases under suitable isolation and execution limits.

Also separate cyclic host-language objects from parsed JSON fixtures. A Python dictionary or JavaScript object can contain identity cycles that a JSON parser does not construct from ordinary input. Testing serialization of those native graphs is useful in its own place, but passing them directly into this validator would test a different input model from the endpoint’s documented JSON contract.

2. Do all descendant nodes enforce the same constraints as the root?

Test invalid data below the root, inside a later sibling, and several levels deep. The recursive node contract must enforce required fields, value types, and object closure at each descendant. A successful root fixture is insufficient evidence, because a generator or wrapper can accidentally replace child validation with a permissive schema while preserving every visible root assertion.

from copy import deepcopy
from jsonschema import Draft202012Validator

valid_tree = {
    "name": "Root",
    "children": [
        {"name": "First", "children": []},
        {"name": "Second", "children": [{"name": "Leaf", "children": []}]},
    ],
}
bad_tree = deepcopy(valid_tree)
bad_tree['children'][1]['children'][0]['name'] = 7
# Load the JSON schema above into a variable named schema.
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
assert validator.is_valid(valid_tree)
assert not validator.is_valid(bad_tree)

Add a descendant with an unknown property and another with a missing children array. Both should fail this specific closed-node contract. The object closure tests explain related restrictions when reusable node definitions are combined with other schemas. Include a scalar or null child as well: items must apply the complete object schema, not just a name constraint that ignores unrelated instance types.

The Draft 2020-12 core specification defines reference evaluation. Test the complete generated schema with your actual validator instead of manually checking a few fields in each object. A home-grown traversal can miss later siblings, validate only one level, or stop early after finding the first plausible node.

Use the JSON Viewer for structural inspection of sanitized examples. It helps locate the second child or a missing array; it does not execute the recursive schema. Keep an expected error location in the validator suite when the library exposes one. The schema error-reporting tests explain how to preserve the distinction between instance locations and schema locations.

If different node categories need different shapes, test their branching policy separately. A heading node, link node, and action node might share some properties but require different fields. Do not make every field optional simply to fit them under one loose recursion rule. A recursive tree can use a carefully specified branch contract, but every branch still needs a valid leaf and targeted invalid descendants.

Add a regression fixture that changes only one leaf while keeping every ancestor valid. When it fails, assert that the reported instance path reaches that leaf rather than stopping at the root. This helps distinguish a deliberate whole-request rejection from a reporting adapter that lost the useful location. Also retain the untouched control tree and assert that neither validation nor diagnostic collection rewrites its names, inserts children arrays, or removes unknown data before the acceptance decision.

Keep fixture construction independent from schema generation where practical. If the generator and the fixture builder repeat the same incorrect assumption, a passing test can merely confirm that they agree. A small handwritten negative fixture at a deep path provides valuable independent evidence without requiring a huge synthetic document.

3. Are depth and total-node budgets enforced independently of local child limits?

Test whole-tree depth and total-node limits as explicit application policies. A maxItems rule bounds one children array, while a single-child chain can grow arbitrarily deep and a shallow tree can still contain many nodes. Define how levels and nodes are counted, then test each boundary with another policy kept comfortably within its allowed range.

def within_tree_budget(root, max_levels=3, max_nodes=5):
    stack = [(root, 1)]
    seen_nodes = 0
    while stack:
        node, level = stack.pop()
        seen_nodes += 1
        if level > max_levels or seen_nodes > max_nodes:
            return False
        if isinstance(node, dict) and isinstance(node.get('children'), list):
            stack.extend((child, level + 1) for child in node['children'])
    return True

This illustrative walker counts the root as level one and counts every candidate node visited through children. The limits of three levels and five nodes are fixture values, not production recommendations. The helper is not a general JSON depth checker, and it does not establish that names, extra properties, or child types are structurally valid.

A three-node chain reaches level three and passes the depth budget. Adding one child creates level four and fails even though each array has only one item. Separately, a root with four leaf children contains five nodes and passes; one more leaf exceeds the node budget while remaining only two levels deep. Preserve both pairs so a resource-policy refactor cannot conflate breadth with depth.

Run a cheap, bounded resource policy before expensive schema evaluation when that is the application’s design, then run structural validation separately. The parsed input has already consumed parser resources, so this walker cannot replace HTTP body limits or parser-level protections. It also follows only the domain’s children edges: an unexpected nested object elsewhere needs the broader request policy to handle it until structural validation rejects it.

The Ajv security guidance discusses resource concerns associated with schema processing and untrusted inputs. Do not infer a universal recursion depth from one library’s successful local run. Measure the complete deployed parser, validator, error collector, and handler with representative permitted inputs. A handler that recursively renders the tree can have a different practical limit from the validator that accepts it.

The iterative walker avoids using Python call recursion for this particular budget check, but it can still grow its explicit stack with a wide children list. Test large-input behavior under controlled limits and consider an early child-count check or a different traversal strategy if your application needs tighter memory guarantees. A short helper is a clear policy demonstration, not a complete denial-of-service defense.

4. Does the packaged application reproduce the same recursive acceptance decisions?

Validate finite fixtures using the schema artifact actually loaded by the deployed application, then exercise the real request wrapper and handler. Check resource-policy and structural failures separately, confirm rejected input does not execute the operation, and preserve the accepted tree unchanged. A local schema object passing tests does not prove packaging, caching, or another consumer uses that same contract.

import json

schema_copy = json.loads(json.dumps(schema))
packaged_validator = Draft202012Validator(schema_copy)
control = {"name": "Leaf", "children": []}
assert within_tree_budget(control)
assert packaged_validator.is_valid(control)
assert not packaged_validator.is_valid({"name": "Leaf"})

This round trip checks a small packaging invariant: the serializable schema still behaves as intended after reconstruction. It is not a deployment test. Keep an additional integration fixture that reads the built schema file from the same location and code path used by your service, rather than importing the test module’s dictionary into both the fixture and the handler.

If packaging wraps the node schema under another object or definition, preserve its resource identity and reference targets. A root-relative reference can change meaning when the schema’s resource boundary changes. The reference checklist covers that problem in detail. Re-run the deep invalid-node fixture after bundling; serialization alone cannot prove that the reference still points to the intended node contract.

The python-jsonschema referencing documentation describes explicit resource registries and retrieval policy. This example is self-contained and needs no remote schema retrieval. If your production schema has external resources, preload the approved definitions and test unavailable targets as configuration failures rather than enabling arbitrary network access to make a fixture pass.

Finally, test downstream tree handling with one accepted control, one structurally invalid descendant, and one structurally valid tree over the application budget. Confirm which stage rejects each case and that only the control reaches the operation. The schema can describe a valid tree without authorizing its contents or deciding which menu actions a caller may create. Keep those business decisions separate and test them at the same endpoint boundary.

Frequently Asked Questions

Can JSON Schema validate a recursive tree?
Yes. A schema can use a reference back to its node definition so each child follows the same structural contract. The submitted JSON tree remains a finite document. Test a leaf, several nested valid nodes, and a bad descendant rather than assuming that a schema accepting the root also evaluates every child correctly.
Is a recursive schema the same as a circular JSON object?
No. Recursive schemas reuse definitions through reference strings while describing finite JSON instances. A circular runtime object links back to an existing object identity, which ordinary JSON cannot directly represent. Keep serialization failures for cyclic native objects separate from validator tests, and avoid feeding cyclic runtime graphs into a validator intended to receive parsed JSON.
Does maxItems set the maximum depth of a recursive tree?
No. maxItems bounds the number of children in an individual array, not the number of ancestor levels in the whole tree. A chain with one child per node can remain within that array limit at every level. Add a deliberate application depth policy or a bounded schema design, then test the permitted boundary and first rejected level.
Does a valid recursive schema guarantee safe execution cost?
No. A structurally valid recursive contract does not establish acceptable resource usage for every input or validator. Bound request size and test application depth and total-node policies independently. Measure the real parser, validator, and handler under representative workloads, and avoid treating a small successful demonstration as a universal safe recursion-depth recommendation.
Can JSON Viewer validate nested recursive rules?
JSON Viewer helps inspect sanitized nested objects, child arrays, and leaf structure. It does not execute recursive JSON Schema references or enforce an application depth budget. Use a compatible validator and explicit resource-policy tests for acceptance decisions, then use structural inspection to investigate a failing descendant without treating a readable tree as proof of validity.

Working with JSON right now?

Paste it into JSON Prettifier to format, validate, or minify it. Everything runs in your browser.

Open JSON Prettifier