JSON Schema if/then/else: Conditional Validation

JSON Schema's if/then/else keywords add conditional validation: if the data validates against the if schema, it must also validate against then; otherwise it must validate against else. This lets a single schema enforce different rules depending on the data's own values — no custom code required.

Want to test a conditional schema right now? Jsonic's JSON Schema Validator fully supports if/then/else and shows the exact error path when validation fails.

Validate conditional schemas in Jsonic

Basic syntax

The three keywords work together as a unit. if is evaluated against the data first. If it passes (the data is valid against the if schema), then the then schema is also applied. If the if schema fails, the else schema is applied instead.

{
  "if":   { "properties": { "type": { "const": "person" } }, "required": ["type"] },
  "then": { "required": ["firstName", "lastName"] },
  "else": { "required": ["companyName"] }
}

In this example, when the data has type: "person", the validator requires firstName and lastName. For any other value of type (or when type is absent), companyName is required instead.

All three keywords are optional — you can use just if/then without else. Omitting else means failing the if condition adds no extra constraints; the data remains valid from the conditional's perspective.

KeywordRoleRequired?
ifThe condition schema — evaluated but not enforced on its ownYes (needed to trigger the conditional)
thenApplied when if passesNo (omit to add no constraints on pass)
elseApplied when if failsNo (omit to add no constraints on fail)

Real-world example: country-specific postal code

One of the most common uses for if/then/else is validating a field's format based on another field's value. Here, the postal code format differs between US ZIP codes and Canadian postal codes:

{
  "type": "object",
  "properties": {
    "country":    { "type": "string" },
    "postalCode": { "type": "string" }
  },
  "required": ["country", "postalCode"],
  "if": {
    "properties": { "country": { "const": "US" } },
    "required": ["country"]
  },
  "then": {
    "properties": {
      "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" }
    }
  },
  "else": {
    "properties": {
      "postalCode": { "pattern": "^[A-Z][0-9][A-Z] [0-9][A-Z][0-9]$" }
    }
  }
}

Valid examples:

{ "country": "US", "postalCode": "10001" }        // passes: US ZIP
{ "country": "US", "postalCode": "10001-1234" }   // passes: US ZIP+4
{ "country": "CA", "postalCode": "K1A 0B1" }      // passes: Canadian format

Invalid examples:

{ "country": "US", "postalCode": "K1A 0B1" }      // fails: Canadian code for US
{ "country": "CA", "postalCode": "10001" }        // fails: US ZIP for Canada

Notice that required: ["country"] is inside the if schema. This is essential: without it, the if would evaluate to true even when country is absent (because a missing property still passes a properties constraint). See the gotchas section below for a full explanation.

Conditional required fields

The most common use case for if/then/else is making fields required based on another field's value — sometimes called "dependent required" validation. The following schema handles three payment methods, each requiring different fields:

{
  "type": "object",
  "properties": {
    "paymentMethod": { "enum": ["card", "bank", "crypto"] },
    "cardNumber":    { "type": "string" },
    "bankAccount":   { "type": "string" },
    "walletAddress": { "type": "string" }
  },
  "required": ["paymentMethod"],
  "if":   { "properties": { "paymentMethod": { "const": "card" } } },
  "then": { "required": ["cardNumber"] },
  "else": {
    "if":   { "properties": { "paymentMethod": { "const": "bank" } } },
    "then": { "required": ["bankAccount"] },
    "else": { "required": ["walletAddress"] }
  }
}

The outer if/then/else handles the "card" branch. The inner (nested) if/then/else inside the outer else handles "bank" vs "crypto". This nesting pattern can be extended to as many branches as needed — though beyond three or four branches, allOf with independent conditions (shown below) is often cleaner.

Valid and invalid examples:

// Valid
{ "paymentMethod": "card",   "cardNumber": "4111111111111111" }
{ "paymentMethod": "bank",   "bankAccount": "GB29NWBK..." }
{ "paymentMethod": "crypto", "walletAddress": "0xABC..." }

// Invalid
{ "paymentMethod": "card" }                  // missing cardNumber
{ "paymentMethod": "bank", "cardNumber": "4111..." }  // missing bankAccount

Multiple independent conditions with allOf

When you need multiple independent conditional rules — conditions that are unrelated to each other and should each apply on their own — wrap each if/then pair inside an allOf:

{
  "allOf": [
    {
      "if":   { "properties": { "isEmployee": { "const": true } } },
      "then": { "required": ["employeeId"] }
    },
    {
      "if":   { "properties": { "hasVehicle": { "const": true } } },
      "then": { "required": ["licensePlate"] }
    }
  ]
}

Both conditions are checked independently. A person who is an employee with a vehicle must provide both employeeId and licensePlate. A non-employee without a vehicle needs neither. An employee without a vehicle only needs employeeId.

Why can't you just write two top-level if/then pairs without allOf? Because JSON object keys must be unique — a JSON Schema document can only have one if keyword at the top level. The second one would silently overwrite the first. allOf is the correct mechanism for combining multiple independent conditional constraints.

A more complex example combining three independent conditions:

{
  "type": "object",
  "properties": {
    "role":       { "enum": ["admin", "user", "guest"] },
    "isVerified": { "type": "boolean" },
    "plan":       { "enum": ["free", "pro", "enterprise"] }
  },
  "allOf": [
    {
      "if":   { "properties": { "role": { "const": "admin" } } },
      "then": { "required": ["adminCode"] }
    },
    {
      "if":   { "properties": { "isVerified": { "const": true } } },
      "then": { "required": ["verifiedAt"] }
    },
    {
      "if":   { "properties": { "plan": { "const": "enterprise" } } },
      "then": { "required": ["contractId", "billingContact"] }
    }
  ]
}

Validation in code: Ajv (JavaScript) and jsonschema (Python)

Both major ecosystem validators fully support if/then/else from Draft-07 onward.

Ajv (JavaScript / Node.js)

const Ajv = require('ajv')
const ajv = new Ajv()

const schema = {
  if:   { properties: { country: { const: 'US' } }, required: ['country'] },
  then: { properties: { postalCode: { pattern: '^[0-9]{5}$' } } },
  else: { properties: { postalCode: { minLength: 3 } } }
}

const validate = ajv.compile(schema)

console.log(validate({ country: 'US', postalCode: '10001' })) // true
console.log(validate({ country: 'US', postalCode: 'SW1A' }))  // false
console.log(validate.errors)
// [{ instancePath: '/postalCode', message: 'must match pattern "^[0-9]{5}$"' }]

Ajv compiles the schema — including all if/then/else branches — at compile time. There is no overhead per-call for evaluating the conditional structure.

jsonschema (Python)

import jsonschema

schema = {
    "if":   {"properties": {"country": {"const": "US"}}, "required": ["country"]},
    "then": {"properties": {"postalCode": {"pattern": "^[0-9]{5}$"}}},
    "else": {"properties": {"postalCode": {"minLength": 3}}}
}

# Passes — US postal code is valid 5-digit ZIP
jsonschema.validate({"country": "US", "postalCode": "10001"}, schema)

# Raises ValidationError — US postal code fails US pattern
try:
    jsonschema.validate({"country": "US", "postalCode": "SW1A"}, schema)
except jsonschema.ValidationError as e:
    print(e.message)  # 'SW1A' does not match '^[0-9]{5}$'

The jsonschema library supports Draft-07 conditionals by default. For Draft 2019-09 or 2020-12 features, pass cls=jsonschema.Draft202012Validator as the validator class.

Common gotchas

if without required: the always-true trap

This is the single most common mistake with if/then/else. An if that only uses properties without required evaluates to true even when the property is absent:

// WRONG: "if" triggers even when "country" is missing
{
  "if":   { "properties": { "country": { "const": "US" } } },
  "then": { "properties": { "postalCode": { "pattern": "^[0-9]{5}$" } } }
}

// Valid:   { "postalCode": "SW1A" }   <-- "if" is true (country absent = no constraint violated)
//          → "then" applies → postalCode must match US pattern → FAILS unexpectedly

// CORRECT: add "required" inside "if"
{
  "if":   { "properties": { "country": { "const": "US" } }, "required": ["country"] },
  "then": { "properties": { "postalCode": { "pattern": "^[0-9]{5}$" } } }
}

With required: ["country"] inside the if, the condition only triggers when country is present and equals "US". When country is absent, the if fails and then is not applied.

else is not an error branch

If the if condition fails, the data is not automatically invalid. The else schema is applied to the data, but if you omit else, there are no additional constraints — the data remains valid from the conditional's perspective. Only constraints you explicitly add in else can cause the data to fail.

Performance: avoid deep nesting

Deeply nested if/then/else chains (more than 3–4 levels) are harder to read and can slow validation on large datasets. For complex discriminated unions with many branches, consider oneOf with an explicit discriminator field — it is often clearer and performs comparably. For independent conditions, always prefer allOf over deep nesting.

Draft compatibility: Draft-07 minimum

if/then/else was introduced in Draft-07. Schemas targeting Draft 04 or Draft 06 cannot use these keywords. If you must support pre-Draft-07 validators, use oneOf with enum constraints or the older not/anyOf pattern to approximate conditional behavior. The JSON Schema composition guide covers the oneOf/anyOf/allOf alternatives in detail.

if/then/else vs oneOf: which to use?

ScenarioBest keywordWhy
Add extra fields based on a flag or enum valueif/then/elseStraightforward conditional; data shape is mostly shared
Validate format/pattern based on another fieldif/then/elseClean expression of "if country is X, postalCode must match Y"
Mutually exclusive data shapes (discriminated union)oneOfEnforces exactly one match; shapes are incompatible
Multiple independent conditions, each adding constraintsallOf with if/thenEach condition applies independently without interference
Pre-Draft-07 compatibility requiredoneOf / anyOfif/then/else is not available

See the JSON Schema composition guide for a deeper look at allOf, anyOf, and oneOf, including their interaction with if/then/else.

Frequently asked questions

What is the difference between if/then/else and oneOf in JSON Schema?

if/then/else is conditional: it applies different validation rules depending on the data's own values, without requiring the data to fully match one exclusive schema. oneOf requires data to match exactly one of the listed schemas — useful for discriminated unions where values are mutually exclusive. if/then/else is simpler when you want to add constraints based on a flag or enum field; oneOf is better when the data shapes are genuinely incompatible.

What happens if I omit the else keyword?

If else is omitted, failing the if condition applies no additional constraints — the data is still valid as far as the conditional is concerned. This is useful when you want to add extra requirements only when a condition is true, without restricting data that does not meet the condition. For example: if isAdmin is true, require adminCode; otherwise, any data is accepted.

How do I make a field required only when another field has a specific value?

Use if with a properties + required check on the trigger field, then put required: ["conditionalField"] in the then clause:

{
  "if":   { "properties": { "type": { "const": "company" } }, "required": ["type"] },
  "then": { "required": ["companyName"] }
}

The required inside if ensures the condition only triggers when the trigger field is present.

Can I chain multiple if/then/else conditions?

Yes — nest them in the else branch: else: { if: ..., then: ..., else: ... }. For independent conditions (both apply simultaneously), use allOf: wrap each if/then pair as an element of allOf. This lets multiple independent conditions each add their own constraints without interfering with each other.

Which JSON Schema drafts support if/then/else?

if/then/else was introduced in JSON Schema Draft-07. It is available in Draft-07, Draft 2019-09, and Draft 2020-12. Drafts 04 and 06 do not have these keywords. If you need to support older validators, use oneOf or anyOf with explicit enum values to achieve conditional behavior.

Why does my if/then/else not trigger when the field is missing?

An if schema that only specifies properties without required evaluates to true even when the property is absent — because an empty object validates against any properties constraint. Always add "required": ["fieldName"] inside your if schema to ensure it only triggers when the field is present and equals the expected value.

Validate your conditional JSON Schema

Paste any schema that uses if/then/else into Jsonic's JSON Schema Validator to check that your conditions fire correctly and see the exact validation error path.

Validate JSON Schema in Jsonic