Skip to content
JSON Schema

6 JSON Schema Property Count Tests Before API Release

·10 min read
6 JSON Schema Property Count Tests Before API Release - JSON Schema guide from JSON Prettifier

Quick answer: Test property-count boundaries, present values, required names, nested scope, name restrictions, and duplicate-name parsing separately. minProperties and maxProperties count members of the object evaluated at that schema location. They do not count schema declarations, identify meaningful values, total a complete document, or recover source names discarded by a parser.

A request can have enough fields and still omit the only field your handler needs. The JSON Schema guide covers the broader model; these six checks isolate count semantics beyond the brief boundary examples in the dynamic-key guide.

Run the Python blocks together, in order, with Python 3.12.14 and jsonschema 4.26.0. They were executed locally using Draft 2020-12 and synthetic values. They do not measure another validator, a framework decoder, or request-resource limits. Wikipedia's associative array overview provides background on key-value mappings; schema behavior comes from the primary references linked below.

1. Do inclusive count boundaries apply only to object instances?

Test objects immediately below, at, and above the accepted count range, then include a non-object fixture. Property-count keywords constrain objects; they do not themselves require the instance to be an object. Add an explicit object type when the endpoint contract needs one, and verify the schema's own keyword values before evaluating requests.

The Draft 2020-12 validation specification defines inclusive bounds and non-negative integer keyword values. The fixture uses a range with two valid boundary counts so an accidental exclusive comparison is visible at either end.

from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError
import json

DIALECT = "https://json-schema.org/draft/2020-12/schema"

def valid(schema, instance):
    document = {"$schema": DIALECT, **schema}
    Draft202012Validator.check_schema(document)
    return Draft202012Validator(document).is_valid(instance)

bounded = {"type": "object", "minProperties": 2, "maxProperties": 3}
for size, expected in [(0, False), (1, False), (2, True), (3, True), (4, False)]:
    assert valid(bounded, {f"field_{i}": i for i in range(size)}) == expected
assert valid({"minProperties": 2}, "not an object")
assert not valid(bounded, "not an object")
assert valid({"type": "object", "maxProperties": 0}, {})
try:
    Draft202012Validator.check_schema({"minProperties": -1})
except SchemaError:
    pass
else:
    raise AssertionError("Negative property bound was accepted")

Keep schema validity separate from instance validity in error reporting. A negative bound is a schema-authoring defect, while an object outside a valid range is an instance failure. Treating both as an ordinary client rejection can hide a broken deployment behind a misleading validation message.

A contradictory positive range can be structurally valid while accepting no objects. For example, a minimum larger than the maximum expresses an impossible object contract rather than a negative keyword value. Review the intended accepted set, not just whether a schema compiler accepts the document.

impossible = {"type": "object", "minProperties": 3, "maxProperties": 2}
Draft202012Validator.check_schema(impossible)
for instance in [{}, {"a": 1, "b": 2}, {"a": 1, "b": 2, "c": 3}]:
    assert not valid(impossible, instance)

This negative control is useful when bounds come from configuration rather than a handwritten schema. A settings migration can invert two otherwise valid integers without producing a schema compilation error. Test the generated schema with at least one known accepted instance as well as known rejections. Otherwise, a release check that confirms only invalid samples fail can pass even after the accepted object set has accidentally become empty.

Retain the non-object negative control when adapting the fixture. It catches wrappers that assume the presence of any object keyword implies object typing. The result should follow the actual schema constraints, not an undocumented convention inferred by the application developer.

2. Are all present members counted regardless of their values?

Test a member containing null, false, zero, an empty string, and empty containers. Each member still contributes one to the containing object's count. Use value constraints to reject empty or unsuitable content; a minimum count is not a completeness score, and a maximum count does not ignore fields simply because their values are falsy.

The official object reference describes member counts. The next fixture deliberately avoids any value constraint so the outcome isolates presence from value quality. Adding unrelated required or type rules would make the reason for rejection less obvious.

exactly_one = {"type": "object", "minProperties": 1, "maxProperties": 1}
for value in [None, False, 0, "", [], {}]:
    assert valid(exactly_one, {"entry": value})
assert not valid(exactly_one, {})
assert not valid(exactly_one, {"entry": None, "other": False})

nonempty_label = {
    **exactly_one,
    "properties": {"entry": {"type": "string", "minLength": 1}},
}
assert not valid(nonempty_label, {"entry": ""})
assert valid(nonempty_label, {"entry": "ready"})

The last two assertions show how a value rule contributes independently. They do not make entry mandatory by name: the count rule still accepts one differently named member unless another constraint forbids it. Keep that limitation visible rather than quietly promoting a teaching schema into a complete endpoint contract.

For a form submission, decide whether a field is absent, explicitly cleared, or supplied with an empty value before serializing it. Those distinctions can change the member count presented to the validator. The null versus undefined guide explains why a JavaScript value present in memory may not survive JSON serialization as a member.

Test the serialized request as well as the client model when counts affect acceptance. A form state object with three keys can produce fewer JSON members after serialization. Conversely, sending explicit nulls can satisfy a count while failing the application's field semantics. Count tests should expose those choices, not encourage clients to add meaningless placeholders just to pass validation.

3. Are mandatory names checked independently from the number of members?

Create fixtures that satisfy the count but omit a required name, and fixtures that include the required name but miss the count. The properties keyword declares checks for matching members; it does not create members or require every declared name. Combine required, count bounds, and unknown-field policies only where the contract needs each behavior.

The required-property reference distinguishes named presence from declarations. Use an unrelated key as a negative control. A normal happy-path sample containing every familiar field cannot reveal a schema that checks only the total.

declared = {
    "type": "object",
    "properties": {"id": {"type": "string"}, "label": {"type": "string"}},
    "minProperties": 1,
}
assert valid(declared, {"unrelated": True})
named = {**declared, "required": ["id"], "minProperties": 2}
assert not valid(named, {"unrelated": True, "another": 0})
assert not valid(named, {"id": "item-42"})
assert valid(named, {"id": "item-42", "label": "Pen"})
assert valid(named, {"id": "item-42", "unrelated": True})

The final assertion is intentional. This schema permits an extra field to supply the second member. If the endpoint requires label specifically, add it to required rather than trying to encode that requirement through a total count. If unrelated names are prohibited, add the relevant object-closure rule and test it separately.

Avoid equating a schema's properties map size with an instance's property count. Adding a new optional property declaration does not add a field to every request. A code generator or form builder may display that declaration, but its presentation behavior is not the meaning of minProperties.

For an API review, write down each independent acceptance claim in plain language: which fields must exist, which optional fields may exist, and how many members are allowed. Map each claim to a fixture. This makes it easier to notice when a count bound is redundant, contradictory, or accidentally counting metadata alongside business inputs.

4. Does each nested object enforce its own local count?

Test a parent with one member whose value is a larger child object. The parent's count remains one; nested members are not added to its total. Apply another count constraint at the child schema when needed, and test the child independently so a valid parent shape cannot conceal an oversized nested map.

Schema keywords evaluate the instance at their applicable location. The python-jsonschema validator documentation describes the validator API used here. The fixture makes both levels explicit instead of inferring document size from one top-level keyword.

parent_only = {"type": "object", "maxProperties": 1}
large_child = {"settings": {"a": 1, "b": 2, "c": 3}}
assert valid(parent_only, large_child)
nested = {
    **parent_only,
    "properties": {"settings": {"type": "object", "maxProperties": 2}},
}
assert not valid(nested, large_child)
assert valid(nested, {"settings": {"a": 1, "b": 2}})
assert not valid(nested, {"settings": {}, "metadata": {}})

Use the JSON Viewer to inspect a sanitized, duplicate-free fixture, expand settings, and distinguish the parent member from the child's members. The viewer helps locate data; it does not execute these schema constraints or prove that a total request-size budget has been enforced.

Decide what a nested map represents before copying limits from its parent. A document with one settings member can hold many options, large strings, or further containers beneath that value. A local member count is useful for a local contract, but it is not a complete measure of memory use or payload complexity.

Keep transport byte limits and nesting policies at their appropriate boundaries. Schema evaluation takes place after an instance exists in the validator, so a top-level maxProperties cannot prevent the parser from allocating a very large child value. The large JSON guide covers broader size considerations without treating one schema keyword as a complete resource defense.

5. Do name rules combine with counts instead of filtering counted members?

Test a count-valid object whose name violates propertyNames, then test an object with acceptable names that exceeds the count. The rules both contribute to overall validity; propertyNames does not select a subset for counting. Likewise, additionalProperties controls unmatched members without making their presence invisible to a separate property-count rule.

The property-name reference defines name validation independently. The two failures below protect different assumptions: a bad name cannot be excused by a small count, and good names do not excuse an oversized object.

named_map = {
    "type": "object", "minProperties": 1, "maxProperties": 2,
    "propertyNames": {"pattern": "^x_[a-z]+$"},
}
assert valid(named_map, {"x_red": 1, "x_blue": 2})
assert not valid(named_map, {"bad": 1})
assert not valid(named_map, {"x_red": 1, "x_blue": 2, "x_green": 3})
assert valid({"type": "object", "maxProperties": 1}, {"bad": 1})

closed = {
    "type": "object", "maxProperties": 2,
    "properties": {"id": True}, "additionalProperties": False,
}
assert not valid(closed, {"id": "item-42", "extra": 1})

The final object has only two members and still fails because one name is not allowed. Do not report every rejection under this schema as too many fields. Preserve the relevant validator keyword and safe instance location in diagnostics so clients can fix the actual rule that failed.

If only extension fields should count toward a quota, a flat object mixes that business requirement with other members. A nested extensions map can make the constrained scope explicit. Alternatively, use a documented application-level rule. Do not describe maxProperties as counting only keys that matched a pattern when it does not perform that selection.

Review composed schemas with the same discipline. A count rule alongside other constraints remains a rule about its current object instance, not a subtotal of fields declared by one branch. The object closure tests cover branch-sensitive allowed-property behavior separately from these total-member checks.

6. Is the duplicate-name policy enforced before parsed-object counting?

Pass repeated source names through the actual decoder before relying on property counts. If parsing collapses duplicate names into one mapping member, the validator can count only that surviving member. A maximum is not a duplicate detector, and a minimum does not count repeated source occurrences as separate fields in the parsed instance.

Python's repeated-name documentation explains its default behavior and decoding hook. This fixture keeps the original source text because reserializing an already parsed dictionary would erase the evidence needed by the duplicate policy.

raw = '{"id":1,"id":2}'
decoded = json.loads(raw)
assert decoded == {"id": 2}
assert valid({"type": "object", "maxProperties": 1}, decoded)
assert not valid({"type": "object", "minProperties": 2}, decoded)

def unique_pairs(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate name")
        result[key] = value
    return result

try:
    json.loads(raw, object_pairs_hook=unique_pairs)
except ValueError:
    pass
else:
    raise AssertionError("Duplicate-name policy did not reject the source")
print("All six property-count test groups passed")

The Python JSON decoding policy tests expand that boundary with nested duplicates and other decoder choices. Keep the same decoder in integration coverage as the one used by the endpoint. A strict helper in a unit test provides little protection if the framework has already decoded the request with different settings.

Finally, run these six groups alongside representative endpoint fixtures when changing schemas, validators, or request preprocessing. Record whether a failure comes from decoding, member count, required names, value constraints, or allowed-property rules. That separation keeps a simple field-count requirement understandable without pretending it proves every other property of a JSON request.

Frequently Asked Questions

Does minProperties make a named field required?
No. minProperties checks how many members an object has, not which names appear. An unrelated member can satisfy a minimum count unless another constraint rejects it. Use required for mandatory names, then test both the count rule and the named-field rule against objects that deliberately satisfy only one of them.
Do null-valued properties count toward maxProperties?
Yes. A member with a null value still exists and contributes to the object's property count. The same applies to false, zero, empty strings, and empty containers. If those values are unacceptable, constrain the relevant property values separately rather than assuming a count rule measures how many fields contain useful data.
Does maxProperties count all nested object members?
No. A property-count constraint applies to the object instance evaluated at that schema location. A nested object is one value of its parent's member; its own members need their own applicable constraints. Test a small parent with a large child to expose assumptions about a document-wide total that the keyword does not enforce.
Does propertyNames filter which members are counted?
No. propertyNames validates names, while minProperties and maxProperties count members on the object being evaluated. A rejected name still exists in that instance; the constraints combine to determine validity. If only a particular category of business fields should be counted, define a different data shape or a separate application rule.
Can maxProperties detect duplicate names from the original JSON text?
Not after a parser has collapsed repeated names into one mapping entry. The validator sees the parsed object, so a property-count rule cannot reconstruct discarded pairs. Enforce any duplicate-name policy during decoding and test that boundary separately, then apply property counts and other schema constraints to the accepted parsed value.

Can you inspect the object at the constrained path?

Use a sanitized, duplicate-free fixture in JSON Viewer to inspect parent and child objects before running schema assertions.

Open JSON Viewer