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.
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.
| Keyword | Role | Required? |
|---|---|---|
if | The condition schema — evaluated but not enforced on its own | Yes (needed to trigger the conditional) |
then | Applied when if passes | No (omit to add no constraints on pass) |
else | Applied when if fails | No (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 formatInvalid examples:
{ "country": "US", "postalCode": "K1A 0B1" } // fails: Canadian code for US
{ "country": "CA", "postalCode": "10001" } // fails: US ZIP for CanadaNotice 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 bankAccountMultiple 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?
| Scenario | Best keyword | Why |
|---|---|---|
| Add extra fields based on a flag or enum value | if/then/else | Straightforward conditional; data shape is mostly shared |
| Validate format/pattern based on another field | if/then/else | Clean expression of "if country is X, postalCode must match Y" |
| Mutually exclusive data shapes (discriminated union) | oneOf | Enforces exactly one match; shapes are incompatible |
| Multiple independent conditions, each adding constraints | allOf with if/then | Each condition applies independently without interference |
| Pre-Draft-07 compatibility required | oneOf / anyOf | if/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.