Skip to content
Guides

5 JSON Schema Default Value Tests Before API Release

·9 min read
5 JSON Schema Default Value Tests Before API Release - Guides guide from JSON Prettifier

Quick answer: Test JSON Schema defaults as a data-processing policy, not an automatic consequence of validation. These five checks distinguish missing fields from explicit values, reject unusable defaults, define nested-object behavior, and verify repeatability. The examples select Draft 2020-12 and use an explicit Python normalizer so every transformation is visible.

Your API documentation shows a page size of 25. A client omits the field, validation fails, and someone proposes turning on a validator option. Before making that switch, test what happens to the request and what downstream code actually receives.

The general schema validation checklist introduces the annotation-versus-mutation distinction. This article turns it into a focused fixture suite. Wikipedia’s background on defaults explains the broader computing concept; the linked specifications and implementation documentation establish the behavior discussed here.

1. Does a missing required field still fail without an explicit default-setting step?

A default annotation does not satisfy required by inserting a missing property during ordinary validation. Test an empty object, a supplied valid value, and a supplied invalid value with mutation disabled. Then test your explicit normalization step separately, so the release suite distinguishes the caller’s original request from the effective object used by application code.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "pageSize": {"type": "integer", "minimum": 1, "maximum": 100, "default": 25}
  },
  "required": ["pageSize"],
  "additionalProperties": false
}

For this schema, an ordinary nonmutating validator rejects {}. It accepts {"pageSize":25} and rejects {"pageSize":0}. The annotation describes a default value; it does not change the required assertion. The official annotations reference explicitly separates default metadata from filling missing values.

Choose the public contract before choosing a library option. If omission is allowed, describe the application’s effective value and where it is supplied. If callers must send the field, retain the missing-field failure. Both contracts are possible, but an undocumented switch between them changes client behavior and makes errors difficult to interpret.

For an omission-friendly endpoint, a practical pipeline can validate an input schema that permits absence, apply a documented default to a copy, and validate a stricter effective schema. For the narrower demonstration below, normalization occurs before the final validation. That choice is visible in the code and should be visible in your API documentation too.

Keep creation and update endpoints separate in this decision. On a create request, omission can reasonably select a documented initial value. On a partial update, omission can mean preserve the stored value. Applying creation defaults to every update can reset a customer’s settings even though the client never requested that change. Give those endpoints separate fixtures, including an existing nondefault value that must survive an unrelated update.

Store an assertion that the original object remains unchanged. Middleware, logging, signature checks, and audit code can share a request reference. A validator returning true does not tell those consumers whether another component inserted fields. Explicit copies make the boundary reviewable and prevent a missing-value convenience from quietly rewriting the evidence your service originally received.

2. Are null, false, zero, and empty strings preserved as supplied values?

Default only absent properties when the policy is based on presence. Test null, false, zero, and an empty string as explicit inputs rather than treating them as missing through a truthiness check. Preserve each supplied value, then let its schema decide validity; a fallback must not silently rescue an invalid request by replacing it.

from copy import deepcopy

def apply_page_size_default(payload):
    if not isinstance(payload, dict):
        raise TypeError("Expected an object")
    result = deepcopy(payload)
    if "pageSize" not in result:
        result["pageSize"] = 25
    return result

The distinction is easy to miss in a shorthand expression. A fallback based on payload.get("pageSize") or 25 replaces more than absence: zero, false, null, and an empty string all take that path in Python. That may look convenient, but it hides the difference between a client omitting a value and explicitly sending an invalid one.

With the integer schema above, all four explicit examples should fail final validation. The normalizer preserves them, which gives the validator a chance to report the actual problem. A nullable field in another endpoint might accept null deliberately; that does not justify replacing null everywhere. Decide per field rather than adopting a global truthiness rule. The JSON null and undefined guide explains the representation boundary behind missing fields and explicit null values.

Implementation-specific behavior needs its own fixture set. Ajv’s mutation documentation describes useDefaults and an empty-value mode that also treats null and empty strings as missing. These are optional tool behaviors, not universal JSON Schema semantics. The Python helper here deliberately uses a narrower, presence-only policy.

Include both the transformed value and final validity in expected results. A test asserting only that normalization returned an object can pass while the client’s zero became 25. A test asserting only that validation passed can hide the same mistake. Together, those assertions expose whether a request was accepted because it was valid or because a transformation changed its meaning.

3. Can a default annotation contain a value that the schema would reject?

Validate proposed default values explicitly because an annotation is not guaranteed to satisfy its surrounding constraints. Test an intentionally invalid default alongside a valid supplied instance, then confirm that applying the invalid default makes the effective instance fail. This catches schemas that compile successfully while advertising a fallback your endpoint cannot safely accept.

from jsonschema import Draft202012Validator

value_schema = {"type": "integer", "minimum": 1, "default": 0}
Draft202012Validator.check_schema(value_schema)
assert Draft202012Validator(value_schema).is_valid(3)
assert not Draft202012Validator(value_schema).is_valid(value_schema["default"])

Schema checking and instance checking answer different questions. The first checks whether the schema is structurally acceptable for its dialect. The second checks whether a particular value meets its constraints. Do not interpret successful schema loading as evidence that every descriptive example or default is a usable production value.

The Draft 2020-12 validation specification recommends valid defaults but does not make that a required assertion. Your application can impose a stronger release rule: reject a schema change when its declared fallback fails the same value constraints clients must satisfy. That is an engineering policy layered on the specification.

Test the full object after applying defaults too. A locally valid field can still conflict with another field, a dependency, or a conditional branch. For example, a numeric limit may be valid in isolation while a request mode requires a different range. The conditional validation tests provide patterns for checking those cross-field boundaries.

Avoid a generic recursive walker that assumes every object containing a default keyword describes one independent instance location. References, composition, and context can change how a subschema applies. Start with the defaults your application actually uses, test their local definitions, and then run complete request fixtures. A smaller explicit policy is easier to review than a universal transformation that accidentally invents semantics.

4. Is creation of a missing nested object an explicit application decision?

A child’s default annotation does not automatically create an absent parent object. Test the parent missing, the parent present but empty, the parent explicitly null, and a fully supplied child. Decide whether normalization should create the parent at all, and avoid recursive construction that turns optional configuration into active behavior without a documented product rule.

{
  "type": "object",
  "properties": {
    "retry": {
      "type": "object",
      "properties": {"attempts": {"type": "integer", "minimum": 1, "default": 3}},
      "required": ["attempts"]
    }
  }
}

Under ordinary validation, {} passes because retry is optional. {"retry":{}} fails because the present retry object lacks attempts. {"retry":null} fails the object type, while {"retry":{"attempts":3}} passes. Those four outcomes establish the baseline before a normalizer changes any data.

A product might interpret an absent retry section as “retry is disabled” and a present section as “configure retries.” Automatically creating retry would then activate behavior the caller never requested. Another product might intentionally enable a bounded retry policy by default. The schema alone cannot settle that product decision, so name it in the configuration contract and test both branches.

If your policy fills attempts only inside an existing object, check that retry exists and is an object before inserting anything. Preserve null for validation rather than replacing it with an empty dictionary. Also create a fresh nested object for each normalized request; otherwise, later mutation can leak settings between requests through a reused mutable default.

The python-jsonschema FAQ discusses default-setting extensions and nested-object implications. Use that as implementation context, not as a reason to silently extend the validator. Keep the application policy small, make construction decisions visible, and include nested fixtures when updating the schema or swapping the library that loads it.

5. Is normalization repeatable and validated before downstream use?

Run normalization on a copy, validate the effective result, and verify that applying the same policy again produces an equal result. Test invalid supplied values as well as missing ones. Record the schema dialect, dependency configuration, and transformation order so a successful local demonstration can be reproduced by the API deployment and its callers.

This harness uses the helper from check two and the schema from check one, loaded into a Python variable named schema. It deliberately does not implement a general JSON Schema default interpreter. The fixed page-size policy is application code, and Draft202012Validator performs the independent validation step after that policy runs.

validator = Draft202012Validator(schema)
for raw, expected in [
    ({}, True),
    ({"pageSize": 25}, True),
    ({"pageSize": 0}, False),
    ({"pageSize": None}, False),
    ({"pageSize": False}, False),
    ({"pageSize": ""}, False),
    ({"pageSize": 101}, False),
]:
    before = deepcopy(raw)
    effective = apply_page_size_default(raw)
    assert raw == before
    assert apply_page_size_default(effective) == effective
    assert validator.is_valid(effective) is expected

Repeatability is useful because request processing can cross several layers. A queue consumer may receive a payload that an API handler already normalized. A second pass should not append another value, overwrite a valid setting, or turn an error into success. Equal results do not prove the whole business process is idempotent; they establish only this transformation’s stable behavior.

Use JSON Compare to inspect sanitized before-and-after fixtures when a result surprises you. It helps show structural changes, but does not apply defaults or certify the policy. Preserve executable assertions in the service repository so a reviewer can reproduce the change without a browser session or a manually edited sample.

Add one integration test at the real application boundary. Submit an omitted page size through the same request adapter used in production, capture the effective value supplied to the handler, and confirm that the handler receives 25. Submit zero through that adapter and confirm the handler is never called. This pair catches configuration drift that isolated helper tests cannot see, including a bypassed normalizer or a validator attached to the wrong stage of the request pipeline.

Finally, carry the effective configuration to downstream code deliberately. Do not validate one object and then execute against an older reference that still lacks the inserted setting. Keep original input for appropriate diagnostics, avoid logging sensitive values, and pass the validated effective instance to the operation that needs it. That final handoff closes the gap between a convincing schema example and a dependable API release.

Frequently Asked Questions

Does JSON Schema default fill missing fields?
The default keyword is an annotation and does not require ordinary schema validation to insert missing fields. Some validators and form tools implement additional default-setting behavior. Treat that behavior as a separate feature with explicit configuration, and test the actual object your application receives rather than assuming validation also performs normalization.
Can a property be both required and have a default?
Yes. A schema can declare a required property and attach a default annotation to its definition. Without a separate default-setting step, an instance missing that property still fails the required constraint. Decide whether callers must provide it or your application supplies it, then test the chosen order of transformation and validation.
Should a default replace an explicit null or false value?
Only if your application contract deliberately defines that replacement. Missing, null, false, zero, and an empty string are different cases. A presence-based defaulting function preserves supplied values and lets validation accept or reject them. Tool-specific empty-value modes can behave differently, so record the configuration and test every supported input category separately.
Does checking a schema prove its default values are valid?
Not necessarily. A schema can contain a default annotation that does not satisfy the surrounding value constraints. Test each default against the appropriate value schema, and validate the complete normalized object as well. Local validity does not prove that a default satisfies cross-field conditions, dependencies, or the business rules of a request.
Can JSON Compare verify default-setting behavior?
JSON Compare can help inspect structural differences between an original JSON payload and a normalized copy. It does not execute JSON Schema defaults or prove your normalization policy is correct. Use automated assertions for accepted values, rejected values, and mutation boundaries, then use the comparison tool to investigate unexpected changes in sanitized fixtures.

Working with JSON right now?

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

Open JSON Prettifier