How to Validate At Least One Enum Value in AJV Arrays

Summary

AJV cannot natively enforce “at least one occurrence of a specific enum value” in an array without custom keywords or dependent schemas. The question arises when a developer has an array of objects, each with a name field that must be one of several enum values, and they want to guarantee that at least one object in the array has name: "file". This is a common pattern in attribute-set or tag-based APIs.

Root Cause

  • The standard enum keyword in JSON Schema only restricts individual values to a whitelist.
  • There is no built-in keyword that says “at least N items in this array must match this specific value.”
  • AJV follows the JSON Schema specification faithfully, so it does not provide this logic out of the box.
  • Developers often assume enum combined with minItems or uniqueItems would help, but none of these achieve the “at least one of X” requirement.

Why This Happens in Real Systems

  • APIs frequently accept arrays of heterogeneous tagged items where some tags are mandatory (e.g., every document must have a file entry).
  • Schema authors write enum to constrain each item, then forget to enforce cross-item constraints.
  • JSON Schema was not designed for aggregate constraints across array members.
  • Teams discover this gap only in integration testing or production when invalid payloads slip through.

Real-World Impact

  • Silent data corruption — downstream consumers assume the required tag exists and crash or produce wrong results.
  • Increased bug surface — the bug hides behind the enum check, which passes even when the required value is missing.
  • Debugging cost — tracing why a consumer failed reveals the schema didn’t catch the missing mandatory tag.
  • Security implications — if file represents a required attachment or credential, its absence can break authentication flows.

Example or Code

The following demonstrates two approaches: a dependent schema workaround and a custom AJV keyword.

const Ajv = require("ajv")
const ajv = new Ajv({ strict: false })

// Approach 1: dependent schema (minItems + every possible name = file check)
const schemaDependent = {
  type: "array",
  minItems: 1,
  items: {
    type: "object",
    properties: {
      name: {
        type: "string",
        enum: ["file", "summary", "description", "function"],
      },
    },
    required: ["name"],
  },
  // This ensures at least one item has name === "file"
  contains: {
    type: "object",
    properties: {
      name: { const: "file" },
    },
    required: ["name"],
  },
}

// Approach 2: custom keyword for reusable "atLeastOneOf" logic
ajv.addKeyword({
  name: "atLeastOneOf",
  type: "array",
  validate: (schema, data) => {
    // schema is an array of allowed values; data is the array being validated
    return data.some((item) => {
      const name = item.name
      return schema.includes(name)
    })
  },
})

const schemaCustom = {
  type: "array",
  minItems: 1,
  items: {
    type: "object",
    properties: {
      name: {
        type: "string",
        enum: ["file", "summary", "description", "function"],
      },
    },
    required: ["name"],
  },
  atLeastOneOf: ["file"],
}

const validData = [
  { name: "summary" },
  { name: "file" },
  { name: "description" },
]

const invalidData = [
  { name: "summary" },
  { name: "description" },
]

console.log(ajv.validate(schemaDependent, validData))   // true
console.log(ajv.validate(schemaDependent, invalidData)) // false
console.log(ajv.validate(schemaCustom, validData))      // true
console.log(ajv.validate(schemaCustom, invalidData))    // false

How Senior Engineers Fix It

  • Use contains in JSON Schema Draft 2019+ / 2020+ to require at least one matching item.
  • Add a custom AJV keyword for reusable cross-array validation logic.
  • Layer validation: run the enum check first, then a separate predicate check for the aggregate constraint.
  • Write integration tests that explicitly assert the presence of the mandatory tag.
  • Document the invariant in the schema description so future maintainers know why contains or the custom keyword exists.

Why Juniors Miss It

  • Junors assume enum on the property is sufficient and never look for cross-property or cross-item constraints.
  • The contains keyword is relatively new and not widely known.
  • No compiler or linter warns when the aggregate invariant is missing; the schema is syntactically valid.
  • Tutorials and docs focus on per-value validation, not on array-level business rules.
  • Juniors conflate “all values are from the enum” with “the required value is present,” which are two different guarantees.

Leave a Comment