Automattic/mongoose · error · Error

Query filter must be an object, got an array ${util.inspect(

Error message

Query filter must be an object, got an array ${util.inspect(obj)}

What it means

Thrown by Mongoose's internal cast() (lib/cast.js) when the object passed as a query filter is an array. Query filters must be plain documents; arrays are rejected at the very top of casting before schema path resolution. The error is a plain Error (not MongooseError) and includes util.inspect of the offending array.

Source

Thrown at lib/cast.js:38

const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];

/**
 * Handles internal casting for query filters.
 *
 * @param {Schema} schema
 * @param {object} obj Object to cast
 * @param {object} [options] the query options
 * @param {boolean|"throw"} [options.strict] Whether to enable all strict options
 * @param {boolean|"throw"} [options.strictQuery] Enable strict Queries
 * @param {boolean} [options.sanitizeFilter] avoid adding implicit query selectors ($in)
 * @param {boolean} [options.upsert]
 * @param {Query} [context] passed to setters
 * @api private
 */
module.exports = function cast(schema, obj, options, context) {
  if (Array.isArray(obj)) {
    throw new Error('Query filter must be an object, got an array ' + util.inspect(obj));
  }

  if (obj == null) {
    return obj;
  }

  if (schema?.discriminators != null && obj[schema.options.discriminatorKey] != null) {
    schema = getSchemaDiscriminatorByValue(schema, obj[schema.options.discriminatorKey]) || schema;
  }

  const paths = Object.keys(obj);
  let i = paths.length;
  let _keys;
  let any$conditionals;
  let schematype;
  let nested;
  let path;
  let type;

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Pass a single object: Model.find({ name: 'x' })
  2. For matching any of several filters use $or: Model.find({ $or: [{ name: 'x' }, { age: 1 }] })
  3. Validate/filter request bodies before forwarding: reject or wrap non-object filters at the API boundary

Example fix

// before
const filters = [{ name: 'x' }, { age: 1 }];
await Model.find(filters);

// after
await Model.find({ $or: [{ name: 'x' }, { age: 1 }] });
Defensive patterns

Strategy: type-guard

Validate before calling

function isPlainFilter(v) {
  return v != null && typeof v === 'object' && !Array.isArray(v);
}
function normalizeFilter(v) {
  if (!isPlainFilter(v)) {
    throw new TypeError(`Query filter must be an object, got ${Array.isArray(v) ? 'array' : typeof v}`);
  }
  return v;
}
await Model.find(normalizeFilter(req.body.filter));

Type guard

const isPlainFilter = (v) =>
  v != null && typeof v === 'object' && !Array.isArray(v);

Try / catch

try {
  await Model.find(filter);
} catch (err) {
  if (err.message.startsWith('Query filter must be an object')) {
    return res.status(400).json({ error: 'filter must be a JSON object' });
  }
  throw err;
}

Prevention

When it happens

Trigger: Model.find([{ name: 'x' }]) (array wrapping the filter); Model.find([{ name: 'x' }, { age: 1 }]) (intending multiple filters); Model.updateOne([{ a: 1 }], { $set: { b: 2 } }); findOne(someArray) where an API sent JSON array as the filter; spread of an array variable into the filter position.

Common situations: Frontends sending JSON arrays where the backend forwards req.body straight into find(); intending OR-semantics and assuming an array means $or; mixing up argument order (filter, update, options); copying driver examples that pass arrays to aggregate() (legal) into find() (illegal).

Related errors


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/97ee509ca9c8cbd8. Report an issue: GitHub.