Skip to content
Advanced

6 JSON Schema Conditional Validation Tests Before API Release

·9 min read
6 JSON Schema Conditional Validation Tests Before API Release - Advanced guide from JSON Prettifier

Quick answer: Before releasing an API with conditional JSON Schema rules, test both sides of every condition, missing discriminators, one-way property dependencies, full dependent subschemas, and boundary counts for matching array items. These six tests expose contracts that compile cleanly but activate the wrong branch or silently skip the intended constraint.

Conditional validation is where a readable schema can hide an unexpected control flow. A single omitted required, misplaced else, or one-way dependency can accept the wrong payload without producing a schema error. Use the six tests below as executable release fixtures, not as a substitute for checking the exact dialect and validator deployed by your API.

1. Does each if/then/else branch accept and reject the right payload?

Test at least one payload that makes if succeed and one that makes it fail, then verify the corresponding then or else constraint. A passing happy path proves only half the rule. Include a failure inside each branch so the suite detects an ignored, misplaced, or accidentally permissive subschema.

The three keywords work together in the same schema object. The official conditional-validation reference expresses their behavior as a truth table: if selects a branch, and the selected branch determines whether the instance passes. When if is missing, then and else are ignored rather than applied independently.

Consider an order whose payment details depend on paymentMethod:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "paymentMethod": { "enum": ["card", "invoice"] },
    "cardToken": { "type": "string", "minLength": 1 },
    "purchaseOrder": { "type": "string", "minLength": 1 }
  },
  "required": ["paymentMethod"],
  "if": {
    "properties": { "paymentMethod": { "const": "card" } },
    "required": ["paymentMethod"]
  },
  "then": { "required": ["cardToken"] },
  "else": { "required": ["purchaseOrder"] }
}

A compact fixture matrix makes the intended decision visible:

PayloadExpectedReason
{"paymentMethod":"card","cardToken":"tok_1"}Passif succeeds and the then requirement is present
{"paymentMethod":"card"}FailSelected then branch lacks cardToken
{"paymentMethod":"invoice","purchaseOrder":"PO-9"}Passif fails and the else requirement is present
{"paymentMethod":"invoice"}FailSelected else branch lacks purchaseOrder

Run all four through the same validator initialization used by production. A schema playground and a server can disagree when they use different drafts, options, or supported vocabularies. The $schema declaration selects Draft 2020-12 semantics; it does not prove that every consumer implements them correctly.

2. Does the if condition require its discriminator to exist?

Put required inside an if subschema when branch selection depends on a discriminator property. The properties keyword evaluates a named property only when it exists, so an absent field can otherwise make the condition succeed. Test a payload with no discriminator and decide whether the root schema or a deliberate fallback should reject it.

This is a subtle consequence of ordinary object validation, not a special exception for conditionals. A schema such as {"properties":{"country":{"const":"CA"}}} is valid for an object without country, because there is no value to test. Used as if, that result selects then.

The payment example protects itself twice: root required makes the public contract explicit, and required inside if makes the branch condition self-contained. That duplication is useful when a conditional fragment is moved or reused. It also keeps error diagnosis honest: a missing discriminator is different from a present but unsupported value.

Add three negative fixtures:

  • no paymentMethod at all;
  • paymentMethod: null;
  • an unrecognized string such as "cash".

The root enum rejects the last two. The root required rejects the first. Without the enum, an unknown string merely falls into else, which might accidentally turn “anything except card” into “invoice.” If that fallback is intentional, document it. If it is not, constrain the discriminator before selecting a branch.

For easier inspection, paste a non-sensitive fixture into the JSON Viewer and confirm the key is at the object level your schema evaluates. The viewer helps reveal nesting and spelling; it does not perform JSON Schema validation.

3. Do dependentRequired rules cover the intended direction?

Test each property-dependency direction explicitly. dependentRequired is one-way: a rule triggered by creditCard can require billingAddress, but the presence of billingAddress does not require creditCard unless you add the reverse mapping. Include trigger-only, companion-only, both-present, and neither-present fixtures before calling the relationship mutual.

Draft 2020-12 can express a simple pairing without an if block:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "creditCard": { "type": "string" },
    "billingAddress": { "type": "string" }
  },
  "dependentRequired": {
    "creditCard": ["billingAddress"]
  }
}

With that schema, {"billingAddress":"Pune"} passes. That may be exactly right: an address can exist for reasons unrelated to a card. If the two properties must always appear together, add "billingAddress": ["creditCard"] as a separate entry.

The Draft 2020-12 validation vocabulary defines dependentRequired in terms of property presence. It does not inspect whether a present string is empty, whether a number is positive, or whether the companion value is useful. Put those value constraints under properties, and test missing, null, empty, and invalid values separately.

Older schemas may use the former dependencies keyword. The official guide notes that Draft 2019-09 split it into dependentRequired and dependentSchemas. Treat that migration as a semantic change worth testing rather than a search-and-replace exercise.

4. Does dependentSchemas enforce the full triggered contract?

Use dependentSchemas when the presence of one property should activate more than companion-field presence. Its value is a complete subschema applied to the whole current instance, so it can require fields and constrain their values together. Test the trigger absent, trigger present with valid details, and every important way the activated details can fail.

Suppose couponCode activates a promotion contract:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "couponCode": { "type": "string", "minLength": 1 },
    "discountPercent": { "type": "integer" }
  },
  "dependentSchemas": {
    "couponCode": {
      "properties": {
        "discountPercent": {
          "type": "integer",
          "minimum": 1,
          "maximum": 50
        }
      },
      "required": ["discountPercent"]
    }
  }
}

Test a coupon with no discount, zero, 51, a string, and a valid boundary value. Also test an object with discountPercent but no couponCode; the shown dependency does not forbid it. If the reverse relationship matters, express it separately instead of assuming symmetry.

The Understanding JSON Schema guide explains that the triggered subschema applies in addition to the main schema, much like an allOf branch. It is not merged into a new object definition. This distinction matters when strict property rules are involved; review the existing JSON Schema validation mistakes before combining conditional fragments with additionalProperties: false.

5. Do missing then or else keywords behave as intended?

Test the unhandled side whenever a conditional omits then or else. A missing branch behaves like an always-valid schema, so the condition may intentionally add a constraint in only one case. Confirm that this permissive path is part of the contract, and ensure neither branch keyword was accidentally placed in another schema object.

An API may require a reason only when a job is cancelled:

{
  "type": "object",
  "properties": {
    "status": { "enum": ["queued", "running", "cancelled"] },
    "reason": { "type": "string", "minLength": 1 }
  },
  "required": ["status"],
  "if": {
    "properties": { "status": { "const": "cancelled" } },
    "required": ["status"]
  },
  "then": { "required": ["reason"] }
}

There is no else, so queued and running jobs face no additional branch constraint. That is different from forbidding reason for those statuses. If the property must be absent outside cancellation, add an explicit else, such as {"not":{"required":["reason"]}}, and test both allowed statuses with and without the field.

Also test schema placement. if, then, and else interact only when they are siblings within one schema object. A then accidentally nested under properties, or placed beside an unrelated if in a different allOf item, does not create the intended pair. The current JSON Schema specification explicitly says that then and else are ignored when if is absent.

6. Do contains, minContains, and maxContains pass every boundary count?

For conditional array counts, test zero matches, one below the minimum, both boundaries, one above the maximum, and arrays with invalid nonmatching items. contains decides which items count; minContains and maxContains limit that count. They do not replace items, which independently governs every array element's permitted shape.

This schema accepts two or three approvers in a reviewers array:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "role": { "enum": ["reviewer", "approver"] },
      "id": { "type": "string", "minLength": 1 }
    },
    "required": ["role", "id"],
    "additionalProperties": false
  },
  "contains": {
    "properties": { "role": { "const": "approver" } },
    "required": ["role"]
  },
  "minContains": 2,
  "maxContains": 3
}

The official array reference identifies minContains and maxContains as Draft 2019-09 additions. The validation specification says they have no effect without an adjacent contains. Declaring the dialect therefore matters when the same schema reaches older tooling.

Do not confuse the number of matches with total array length. Five reviewers plus two approvers passes this conditional count unless another rule limits maxItems. Likewise, items still rejects an object with an unknown role or missing ID even when two other elements satisfy contains.

What should the conditional-validation release checklist verify?

A release suite should prove the active dialect, production validator, true and false branches, missing discriminators, dependency direction, activated value constraints, omitted-branch behavior, and array-count boundaries. Store small named fixtures beside the schema, assert the expected result for each, and rerun them whenever the schema, validator version, or options change.

Use a table or data-driven test rather than one long example. A practical fixture set includes:

  1. the smallest passing payload for each branch;
  2. one failure caused by each branch-specific requirement;
  3. missing, null, empty, and unknown discriminator values;
  4. trigger-only and companion-only dependency cases;
  5. exact minimum and maximum array-match counts;
  6. the same suite under every validator used by producers and consumers.

JSON Schema provides a JSON-based language for describing JSON structure; Wikipedia's JSON overview gives that background, while the official specifications remain the authority for keyword behavior. Parsing valid JSON is only the first gate. A conditional schema can still encode the wrong policy, and a correct schema cannot evaluate permissions, database state, or time-dependent business rules.

Keep those boundaries visible in code. Validate the incoming instance without silently rewriting it, preserve detailed validator errors in internal logs, and return a stable safe error shape to clients. For broader contract coverage, pair this checklist with the JSON Schema primer and the JSON Schema reference checks.

Name fixtures after the behavior they prove, such as card-requires-token.invalid.json or two-approvers.valid.json. Reviewers can then understand a failure without decoding a numbered sample. Keep expected results in source control, and require a deliberate fixture update when policy changes; otherwise, a seemingly harmless schema refactor can broaden or narrow the public API contract unnoticed.

Frequently Asked Questions

Does if require the discriminator property in JSON Schema?
No. The if keyword only evaluates its subschema; it does not automatically require a property named inside properties. Add required within the if subschema when the condition depends on that property's presence. Otherwise, an absent discriminator may satisfy the condition and unexpectedly activate the then branch.
What happens when then or else appears without if?
A conforming JSON Schema implementation ignores then and else when if is absent. They do not behave as standalone validators. Keep the three keywords in the same schema object, declare the intended dialect, and add tests for both the true and false condition paths so misplaced keywords cannot pass unnoticed.
What is the difference between dependentRequired and dependentSchemas?
dependentRequired says that when one property exists, specified companion properties must also exist. dependentSchemas applies an entire subschema when the triggering property exists, so it can enforce types, ranges, nested requirements, or other constraints. Neither dependency is automatically bidirectional; add the reverse rule when the relationship is mutual.
Can JSON Schema require a certain number of matching array items?
Yes. In Draft 2019-09 and later, use contains with minContains and maxContains. contains defines which items count as matches, while the other keywords set the permitted count. Keep all three in the same schema object and test zero, boundary, and over-limit match counts with your production validator.
Should conditional JSON Schema rules replace application business logic?
Use conditional schemas for deterministic structural constraints that validators can evaluate from one JSON instance. Keep database lookups, permissions, inventory checks, calculations, and time-sensitive policy in application code. Schema validation is a contract boundary, not a replacement for rules that require external state or side effects.

Inspect the payload before testing its conditions

Use the JSON Viewer to confirm the field names and nesting, then run the payload through a Draft 2020-12 validator.

Open the JSON Viewer