What it means
In a filter, a name that starts with $ must be a query operator: $eq, $gt, $in, $exists,
$regex, $and, $or and so on. MongoDB found one it doesn’t know and refused the query with code
2, BadValue. Nothing ran.
Where the name sits decides the wording:
unknown operator: $foo: inside a field’s condition, as in{ qty: { $foo: 1 } }.unknown top level operator: $foo. If you have a field name that starts with a '$' symbol, consider using $getField or $setField.: at the top of the filter, as in{ $foo: 1 }.
The same applies to find, countDocuments, deleteMany, the filter of an update, and $match in
an aggregation.
Common causes
- A typo or the wrong case:
$regx,$exist,$Lt. Operators are case-sensitive. - An operator from another language or database:
$like(SQL’sLIKE),$contains,$between,$ilike. - An aggregation expression in a plain filter.
{ $eq: ["$qty", "$count"] }compares two fields only inside$expr; at the top level it’s an unknown operator. - User input passed into the filter as an object. A query-string parser that reads brackets
turns
?name[$foo]=xinto{ name: { $foo: "x" } }. That also means users can inject real operators such as$ne, which is worse. - A field name that really starts with
$, which MongoDB reads as an operator.
How to fix it
Use the real operator
| You wrote | Use |
|---|---|
$regx, $like, $contains | { name: { $regex: "amp", $options: "i" } } or { name: /amp/i } |
$exist | { qty: { $exists: true } } |
$Lt, $GTE | $lt, $gte |
$between | { qty: { $gte: 1, $lte: 10 } } |
$ne on several values | $nin: [ … ] |
A $regex without a leading ^ can’t use an index efficiently, so prefer anchored patterns
(/^amp/) or a text index for word search.
Compare fields with $expr
To compare one field with another, or use aggregation expressions in a filter:
db.p.find({ $expr: { $eq: ["$qty", "$count"] } })
Don’t pass request objects into filters
Cast inputs to the type you expect before they reach the query (String(req.query.name)), or
validate them with a schema. Mongoose’s sanitizeFilter option wraps such objects in $eq, so
they’re compared as values instead of being read as operators.
Read a field whose name starts with $
db.p.find({ $expr: { $eq: [{ $getField: { $literal: "$price" } }, 5] } })
Reproduce it
MongoDB 8.0.32, mongosh 2.12.0:
db.items.find({ qty: { $foo: 1 } })
MongoServerError[BadValue]: unknown operator: $foo
The error’s code was 2 and its codeName BadValue. With the operator at the top:
db.items.find({ $foo: 1 })
MongoServerError[BadValue]: unknown top level operator: $foo. If you have a field name that starts with a '$' symbol, consider using $getField or $setField.
$like, $contains, $exist, $regx and $Lt (beside a valid $gt) each gave
unknown operator: <name>. { $eq: ["$qty", "$count"] } gave unknown top level operator: $eq, and
the same comparison inside $expr ran. A related mistake gave its own message with the same code:
MongoServerError[BadValue]: Failed to parse $size. Expected a number in: $size: { $gt: 0 }
That came from { tags: { $size: { $gt: 0 } } }: $size takes only an exact number. Use
{ "tags.0": { $exists: true } } for “at least one element”.
With Mongoose 8.23.0, a schema path caught { name: { $foo: "x" } } before it was sent:
Error: Can't use $foo with String. With sanitizeFilter on, the same filter, and one with $ne,
failed as a CastError instead, since the object was now a value.
In Inlet
Queries use mongosh syntax, so you can try the corrected filter in a query tab before you change your code. Filters in Inlet’s grid run on the server, and a refused query shows the server’s message with the operator it didn’t know.