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
enumkeyword 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
enumcombined withminItemsoruniqueItemswould 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
fileentry). - Schema authors write
enumto 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
enumcheck, 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
filerepresents 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
containsin 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
containsor the custom keyword exists.
Why Juniors Miss It
- Junors assume
enumon the property is sufficient and never look for cross-property or cross-item constraints. - The
containskeyword 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.