{"record":{"id":"f48d79e429240b5f","repo":"Automattic/mongoose","slug":"conditional-op-requires-an-array","errorCode":null,"errorMessage":"conditional ${op} requires an array","messagePattern":"conditional (.+?) requires an array","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"lib/schema/array.js","lineNumber":637,"sourceCode":" * @memberOf SchemaArray\n * @instance\n * @api public\n */\n\nconst handle = SchemaArray.prototype.$conditionalHandlers = {};\n\nhandle.$all = cast$all;\nhandle.$options = String;\nhandle.$elemMatch = cast$elemMatch;\nhandle.$geoIntersects = geospatial.cast$geoIntersects;\nhandle.$or = createLogicalQueryOperatorHandler('$or');\nhandle.$and = createLogicalQueryOperatorHandler('$and');\nhandle.$nor = createLogicalQueryOperatorHandler('$nor');\n\nfunction createLogicalQueryOperatorHandler(op) {\n  return function logicalQueryOperatorHandler(val, context) {\n    if (!Array.isArray(val)) {\n      throw new TypeError('conditional ' + op + ' requires an array');\n    }\n\n    const ret = [];\n    for (const obj of val) {\n      ret.push(cast(this.embeddedSchemaType.schema ?? context.schema, obj, null, this?.$$context));\n    }\n\n    return ret;\n  };\n}\n\nhandle.$near =\nhandle.$nearSphere = geospatial.cast$near;\n\nhandle.$within =\nhandle.$geoWithin = geospatial.cast$within;\n\nhandle.$size =","sourceCodeStart":619,"sourceCodeEnd":655,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/schema/array.js#L619-L655","documentation":"The handlers mongoose builds for logical operators ($or/$and/$nor) inside array-path query casting require the operator value to be an array of criteria objects; a scalar or bare object cannot express a disjunction/conjunction, so a TypeError is thrown during castForQuery.","triggerScenarios":"`{ arr: { $or: { x: 1 } } }` - a logical operator used per-field instead of at the top level; `{ tags: { $and: 'red' } }`; programmatic filter builders emitting a single object where an array of clauses is required.","commonSituations":"Converting top-level $or/$and into per-field filters by mistake; generic filter code wrapping criteria inconsistently; raw MongoDB shapes that tolerate field-level use but mongoose's caster does not.","solutions":["Move $or/$and/$nor to the filter's top level: `{ $or: [{ a: 1 }, { b: 2 }] }`.","For per-field element clauses, express them with $elemMatch / $all + $elemMatch forms.","Fix filter builders to always emit arrays of clauses for logical operators."],"exampleFix":"// before\nModel.find({ tags: { $or: { $eq: 'red' } } });\n\n// after\nModel.find({ $or: [{ tags: 'red' }, { tags: 'blue' }] });","handlingStrategy":"validation","validationCode":"const LOGICAL = new Set(['$or', '$and', '$nor']);\nconst validateFilter = (filter) => {\n  for (const [k, v] of Object.entries(filter ?? {})) {\n    if (LOGICAL.has(k) && !Array.isArray(v)) throw new TypeError(`${k} requires an array of clauses`);\n    if (k.startsWith('$')) continue;\n    if (v && typeof v === 'object' && !Array.isArray(v)) validateFilter(v);\n  }\n};\nvalidateFilter(query);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["In query-builder code, always wrap logical clauses in arrays, even a single one ([clause]).","Unit-test generated filters against mongoose casting before shipping them.","Keep $or/$and/$nor at the filter's top level unless you specifically need $elemMatch."],"tags":["query","logical-operators","type-mismatch","arrays"],"backgroundTag":"query-operator-type-mismatch","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}