Quick answer: Test dynamic JSON keys by separating name validation from value validation, checking how patterns overlap, reserving fixed fields deliberately, and testing empty and oversized maps. These six checks use Draft 2020-12 fixtures to expose contracts that look strict but accept the wrong metadata, or reject legitimate extension fields.
A metadata endpoint accepts {"x_team":"billing"}. Someone adds {"x_team":17}, and the request still passes. The key follows the convention; the value breaks the contract. That is the boundary this guide tests.
Start with the broader JSON Schema introduction if schema validation is new to your team. Here, the focus is a dictionary with caller-selected names. Wikipedia’s associative array overview explains the general key-to-value model; the schema specifications define the validation behavior below.
1. Does the schema validate names separately from their values?
Use propertyNames to constrain each key string and a separate schema to constrain its associated value. Test these dimensions independently: a valid name with a bad value, a bad name with a valid value, and a pair satisfying both. This prevents a naming convention from being mistaken for a complete metadata contract.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"propertyNames": {"pattern": "^x_[a-z]+$"},
"additionalProperties": {"type": "string", "maxLength": 80}
}
For this map, {"x_team":"billing"} passes. {"team":"billing"} fails the name rule, while {"x_team":17} fails the value rule. Those failures should remain distinguishable in your internal diagnostics, even if the public API returns a simpler validation error. Fixing the name cannot repair the value.
The Draft 2020-12 core specification defines propertyNames as applying a schema to property names. A key is a string for this evaluation. Numeric-looking keys such as "42" do not become numbers merely because a programming language can use numeric indexes elsewhere.
Keep the object type explicit. Object-specific constraints do not establish the top-level type by themselves. Include a string, null, and an array as negative fixtures, alongside ordinary object samples. Otherwise, a wrapper accepting the wrong body shape can create the impression that the map schema is permissive when the type assertion was simply absent.
Inspect a sanitized payload in the JSON Viewer when a key appears correct but fails. It can reveal nesting and spelling differences. It does not run this schema, infer your naming policy, or turn an inspection into a validation result.
2. Does patternProperties constrain selected values without requiring matching entries?
Use patternProperties when a key’s shape selects the schema for its value. Test a correctly named entry with the wrong value type, an unmatched name, and an empty object. Matching patterns constrain existing entries; they do not require a matching entry to exist, and unmatched entries remain allowed unless another keyword restricts them.
{
"type": "object",
"patternProperties": {
"^count_[a-z]+$": {"type": "integer", "minimum": 0}
},
"additionalProperties": false
}
The object {"count_orders":3} passes, while {"count_orders":"3"} fails. A string containing a digit is still a string. The contract does not instruct the validator to coerce it into an integer. {"orders":3} also fails, because the name matches no permitted pattern and additionalProperties rejects that unmatched entry.
An empty object passes this example. If the product requires at least one counter, add minProperties as a separate requirement. If it requires count_orders specifically, use required. A requirement for one entry and a requirement for a particular entry solve different problems, so they deserve different fixtures.
The official object reference distinguishes named properties, patterned properties, and additional properties. A useful review question is whether the map is homogeneous or partitioned. A homogeneous map often needs only propertyNames plus one additionalProperties value schema. Pattern-based partitions help when prefixes such as count_ and label_ select different value types.
Avoid introducing multiple prefixes merely to make the schema look extensible. Each accepted namespace becomes part of the public contract. Document what a new prefix means, who can create it, and whether clients can safely ignore it. Schema validity alone does not settle compatibility or authorization for those entries.
3. Can overlapping name patterns silently create an impossible value contract?
Every patternProperties expression matching a name applies to that property’s value. Test overlapping patterns with values that satisfy the first constraint, the second constraint, and both together. There is no first-match priority, so reordering schema members cannot resolve incompatible types or make a later expression override an earlier validation rule.
{
"type": "object",
"patternProperties": {
"^count_": {"type": "integer"},
"_total$": {"minimum": 0}
},
"additionalProperties": false
}
For count_total, both rules apply. The value 3 passes; -1 fails the minimum; "3" fails the integer type. For count_open, only the integer rule applies, so a negative integer passes this deliberately incomplete example. That difference is a useful fixture: it proves where the minimum actually applies.
The second rule does not require a number on its own. A name such as label_total matches it, and a string value passes because minimum is a numeric assertion. Add a type to that branch if every matching value must be numeric. Tests should demonstrate the current schema rather than silently assume the stronger intended rule.
The core specification’s patternProperties section supplies the matching semantics. Its rule is conjunction across matching schemas, not routing to one chosen branch. A combination requiring both a string and an integer is therefore impossible for that property value.
Review overlaps before generating client types. A code generator’s union type or dictionary declaration cannot repair an inconsistent runtime contract. Keep a small matrix of representative keys with the patterns they match, then add one fixture at each intersection. The intersection is often where a harmless-looking naming change becomes an API regression.
4. Are reserved fields handled by both the naming policy and the value rules?
Fixed properties and dynamic entries share one object, so propertyNames also checks reserved field names. Test each reserved name against the overall naming policy and verify that it avoids unintended dynamic patterns. Define explicit exceptions when necessary, then reject unknown names independently so a fixed-field exception does not accidentally open the entire object.
{
"type": "object",
"propertyNames": {
"anyOf": [{"const": "version"}, {"pattern": "^x_[a-z]+$"}]
},
"properties": {"version": {"const": 1}},
"patternProperties": {"^x_[a-z]+$": {"type": "string"}},
"required": ["version"],
"additionalProperties": false
}
Here {"version":1,"x_team":"billing"} passes. A version of 2 fails. An unknown team property fails, and an object containing only x_team fails because version is required. Keep all four outcomes: together they describe the intended mixed fixed-and-dynamic object more clearly than a single successful request.
Listing version under properties does not exempt it from propertyNames. Without the explicit exception, a naming policy requiring the x_ prefix would reject the reserved field. Conversely, exempting the name does not exempt the value from its own constraints. These are independent layers evaluated against the same entry.
For a flat object, this example keeps additionalProperties beside the properties it recognizes. If your schema spreads fields across references or composition, review the object closure tests before moving that restriction. The right closure keyword depends on how the complete schema is assembled.
Choose reserved names deliberately before clients depend on them. Adding a new mandatory field to a formerly pure metadata map changes the contract. A namespace policy should leave room for future system fields, but “room” is a documented design decision, not a promise that every future payload remains backward compatible.
5. Are name length, entry count, and empty-map rules tested independently?
Bound names with propertyNames constraints and bound map entries with minProperties or maxProperties. Test an empty map, the allowed entry-count boundary, and a name just beyond its length limit. These limits measure different things, and none replaces a request-body byte limit, nested-value limits, or resource controls before parsing and schema evaluation.
{
"type": "object",
"minProperties": 1,
"maxProperties": 2,
"propertyNames": {"minLength": 1, "maxLength": 12},
"additionalProperties": {"type": "string", "maxLength": 80}
}
This contract permits one or two entries. It rejects an empty name even when the object has the right number of entries. It also rejects a thirteen-character ASCII name regardless of how short its value is. Test exactly twelve and thirteen characters so an off-by-one assumption becomes visible.
The validation specification defines string lengths and object property counts separately. These are structural assertions, not estimates of storage cost. A short key can point to a very long value unless you also constrain that value; a small object count can still contain deeply nested data under a permissive value schema.
Keep byte limits at the request boundary. A schema is evaluated after the validator receives an instance, and cannot retroactively prevent the parser from allocating memory for the original body. Similarly, maxProperties counts the parsed instance’s properties; it is not a detector for repeated member names that a parser already discarded.
For portable naming conventions, simple ASCII examples make good baseline fixtures. Add the Unicode names your product actually allows rather than claiming every identifier standard is interchangeable. The separate JSON Schema regex guide covers escaping, boundaries, and engine behavior; this checklist concentrates on how name rules interact with the map contract.
6. Does the deployed validator reproduce the complete key-and-value fixture matrix?
Validate each schema and run a fixture matrix with the dialect used by your API. Include valid input and a targeted failure for every naming, value, overlap, reserved-field, and size rule. Preserve original instances and expected outcomes, then rerun the suite after schema generation or dependency changes instead of checking only successful examples.
Use python-jsonschema’s documented validator interface for a small reproducible harness. Select Draft202012Validator explicitly and record the installed dependency in your project lock or environment manifest. These fixtures exercise the first check; expand the same harness to the remaining schemas rather than relying on visual inspection.
from copy import deepcopy
from jsonschema import Draft202012Validator
schema = {
"type": "object",
"propertyNames": {"pattern": "^x_[a-z]+$"},
"additionalProperties": {"type": "string", "maxLength": 80},
}
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
cases = [
({"x_team": "billing"}, True),
({"team": "billing"}, False),
({"x_team": 17}, False),
({"x_team": "a" * 81}, False),
({}, True),
(None, False),
]
for instance, expected in cases:
before = deepcopy(instance)
assert validator.is_valid(instance) is expected
assert instance == before
Prefer targeted negative fixtures to one payload with many mistakes. If a request violates five constraints, a failure proves only that something was rejected. A single wrong value under an otherwise valid key proves the value rule directly. Keep more complex payloads for integration coverage after the individual boundaries are established.
Finally, compare the complete generated schema with the one your service loads. A template can omit a keyword, an adapter can coerce values, or a different validator can use another dialect. The release decision should rest on observable acceptance and rejection of realistic requests. Formatting makes the JSON readable; executable fixtures establish what the contract actually permits.
Store fixture names with the rule they exercise, such as reserved-version-wrong-value or dynamic-key-wrong-type. When a test fails during review, that name tells the maintainer whether the public contract changed or the implementation drifted. Capture detailed errors internally without echoing private metadata values into public responses or logs. A useful diagnostic can identify the failing schema keyword and instance location while leaving the sensitive value out. If your validation wrapper rewrites input intentionally, test that transformation separately and document the resulting contract instead of presenting it as ordinary schema validation.