JSON Schema oneOf, anyOf, allOf, not, and if/then/else

JSON Schema composition keywords let you combine or constrain schemas. allOf requires every sub-schema to pass, anyOf requires at least one, oneOf requires exactly one, not inverts a schema, and if/then/else applies conditional validation. Together they cover union types, schema extension, discriminated unions, and field-level conditions.

Want to test a composition schema against real data? Jsonic's JSON Schema Validator shows exactly which branch failed and why.

Validate JSON Schema

allOf — all schemas must pass

allOf takes an array of schemas. The data is valid only if it satisfies every schema in the array. This is the standard way to extend a base schema with additional properties or constraints.

The following example merges a Person base schema with an Employee extension. Any valid employee must satisfy both.

{
  "allOf": [
    {
      "type": "object",
      "description": "Person base schema",
      "properties": {
        "name": { "type": "string" },
        "age":  { "type": "integer", "minimum": 0 }
      },
      "required": ["name"]
    },
    {
      "type": "object",
      "description": "Employee extension",
      "properties": {
        "employeeId": { "type": "string" },
        "department":  { "type": "string" }
      },
      "required": ["employeeId", "department"]
    }
  ]
}

Valid data must supply name (from Person) and both employeeId and department (from Employee):

{
  "name": "Alice",
  "age": 31,
  "employeeId": "EMP-001",
  "department": "Engineering"
}

Watch out: if two allOf sub-schemas declare conflicting type constraints (e.g. one says type: string and another says type: integer), no value can ever pass. This is a common mistake covered in the common mistakes section.

anyOf — at least one schema must pass

anyOf passes when one or more of the sub-schemas match. It models union types: the value can be a string or an integer, for example.

{
  "type": "object",
  "properties": {
    "quantity": {
      "anyOf": [
        { "type": "string", "pattern": "^\d+$" },
        { "type": "integer", "minimum": 0 }
      ]
    }
  }
}

Both { "quantity": 5 } and { "quantity": "5" } are valid. If the value matches both sub-schemas, anyOf still passes — that is the key difference from oneOf.

anyOf is also the conventional way to express a nullable field:

{
  "properties": {
    "middleName": {
      "anyOf": [
        { "type": "string", "minLength": 1 },
        { "type": "null" }
      ]
    }
  }
}

oneOf — exactly one schema must pass

oneOf passes when exactly one sub-schema matches — all others must fail. Use it for discriminated unions where the options are mutually exclusive.

The following example validates a payment method that is either a card payment or a bank transfer, but never both at the same time:

{
  "oneOf": [
    {
      "type": "object",
      "description": "Card payment",
      "properties": {
        "type":       { "const": "card" },
        "cardNumber": { "type": "string", "pattern": "^\d{16}$" },
        "expiry":     { "type": "string", "pattern": "^\d{2}/\d{2}$" }
      },
      "required": ["type", "cardNumber", "expiry"],
      "additionalProperties": false
    },
    {
      "type": "object",
      "description": "Bank transfer",
      "properties": {
        "type":          { "const": "bank" },
        "accountNumber": { "type": "string" },
        "routingNumber": { "type": "string" }
      },
      "required": ["type", "accountNumber", "routingNumber"],
      "additionalProperties": false
    }
  ]
}

Because each branch uses additionalProperties: false and a unique const discriminator on type, a card payload cannot accidentally match the bank branch and vice versa.

not — inverse validation

not takes a single schema and passes when that schema fails. It is useful for blocking specific values, types, or patterns.

Block a null value:

{
  "properties": {
    "username": {
      "type": "string",
      "not": { "type": "null" }
    }
  }
}

Block an empty string:

{
  "properties": {
    "slug": {
      "type": "string",
      "not": { "maxLength": 0 }
    }
  }
}

You can also use not with enum to exclude a specific set of values:

{
  "properties": {
    "status": {
      "type": "string",
      "not": { "enum": ["banned", "deleted"] }
    }
  }
}

not does not restrict the type of the value — it only says the value must not match the given schema. Combine it with type when you also need a type constraint.

if/then/else — conditional validation

if, then, and else apply conditional schemas. When the if schema passes, the then schema is also applied. When the if schema fails, the else schema is applied instead. Neither then nor else affects whether the if schema itself passes or fails for the overall document.

A practical example: if the shipping country is "US", then a zip code is required; otherwise a postal code is required.

{
  "type": "object",
  "properties": {
    "country":    { "type": "string" },
    "zipCode":    { "type": "string", "pattern": "^\d{5}$" },
    "postalCode": { "type": "string" }
  },
  "required": ["country"],
  "if": {
    "properties": { "country": { "const": "US" } },
    "required": ["country"]
  },
  "then": {
    "required": ["zipCode"]
  },
  "else": {
    "required": ["postalCode"]
  }
}

Valid US address:

{ "country": "US", "zipCode": "90210" }

Valid non-US address:

{ "country": "CA", "postalCode": "M5H 2N2" }

You can also chain multiple conditions by nesting if/then/else inside the else branch, or by using an allOf that contains several independent if/then pairs:

{
  "allOf": [
    {
      "if":   { "properties": { "role": { "const": "admin" } }, "required": ["role"] },
      "then": { "required": ["adminToken"] }
    },
    {
      "if":   { "properties": { "role": { "const": "user" } }, "required": ["role"] },
      "then": { "required": ["userId"] }
    }
  ]
}

Common mistakes with JSON Schema composition

Conflicting types inside allOf

If one sub-schema says type: "string" and another says type: "integer", no value can pass both. The schema becomes unsatisfiable. Use anyOf instead when you intend a union.

// ❌ Always fails — no value is both a string and an integer
{
  "allOf": [
    { "type": "string" },
    { "type": "integer" }
  ]
}

// ✅ Correct union type
{
  "anyOf": [
    { "type": "string" },
    { "type": "integer" }
  ]
}

Using oneOf when schemas can overlap

oneOf fails if more than one sub-schema matches. If your sub-schemas are not mutually exclusive, a valid value may fail because it satisfies two branches. Use anyOf unless you specifically need the "exactly one" constraint.

Forgetting that not does not imply a type

{ "not": { "type": "null" } } passes for strings, numbers, objects — and for anything that is not null. If you also need the value to be a string, add type: "string" alongside the not.

Missing required inside the if clause

When writing an if condition that checks a property value, you usually need to also include that property in the if schema's required array. Without it, a document that omits the property entirely will still match the if schema (because JSON Schema skips missing properties by default), which can trigger the then branch unexpectedly.

Composition keyword comparison table

KeywordPasses whenTypical use case
allOfAll sub-schemas passMerge base schema + extension; add constraints to an existing schema
anyOfAt least one sub-schema passesNullable field; union type (string or number)
oneOfExactly one sub-schema passesDiscriminated union; mutually exclusive payment methods or event types
notThe sub-schema failsBlock null, empty string, or a specific enum value
if/then/elseif passes → then applies; if fails → else appliesConditional required fields based on another field's value

Frequently asked questions

What is the difference between oneOf and anyOf in JSON Schema?

anyOf passes when at least one sub-schema matches. oneOf passes only when exactly one sub-schema matches — all others must fail. Use anyOf for nullable fields or general union types. Use oneOf for discriminated unions where options are truly mutually exclusive, such as a payment object that is either a card payment or a bank transfer but never both.

How do I combine two JSON schemas with allOf?

Put both schemas in an allOf array. The validated data must satisfy every schema in the array. This is the conventional way to extend a base schema with additional properties or stricter constraints without modifying the original schema definition.

{
  "allOf": [
    { "$ref": "#/$defs/BaseProduct" },
    {
      "properties": { "discount": { "type": "number", "minimum": 0 } },
      "required": ["discount"]
    }
  ]
}

When should I use oneOf vs anyOf for nullable fields?

Use anyOf. Both produce the same result for nullable fields because a null value can only satisfy the null sub-schema and a string value can only satisfy the string sub-schema — there is no overlap. Convention favors anyOf because it is semantically clearer ("the value can be one of these types") and avoids surprising failures if sub-schemas are later extended.

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

Use if/then. Put the condition in if (include the property in that schema's required to avoid matching absent fields) and put the conditional required in then. Add an else branch if a different set of fields should be required when the condition is false.

Can I nest composition keywords in JSON Schema?

Yes. Composition keywords can be nested to any depth. An allOf can contain branches that each use anyOf, or an if/then/else can appear inside a oneOf branch. Keep nesting shallow to maintain readability and to make validator error messages easier to interpret.

Does JSON Schema allOf merge properties from multiple schemas?

Conceptually yes — because the data must satisfy all sub-schemas simultaneously, it effectively has to supply all properties each sub-schema requires. Validators evaluate each sub-schema independently and report errors from any that fail. There is no literal schema-merge step; the intersection is enforced through validation, not through combining schema objects.

Can I use not with anyOf or oneOf?

Yes. For example, { "not": { "anyOf": [{"type":"null"}, {"maxLength":0}] } } rejects both null values and empty strings in a single keyword. Combining not with composition keywords gives you expressive exclusion logic without enumerating every allowed value.

Test your composition schema

Paste any allOf, anyOf, oneOf, or if/then/else schema into Jsonic's JSON Schema Validator to see which branch passes, which fails, and the exact path of every error.

Open Schema Validator