Element and Type Operators

Two operators ask about a field itself rather than its value, and they are how you cope with a flexible schema. $exists: true matches a field that is present even when its value is null, $exists: false an absent one. $type takes a type alias, a numeric BSON code, or an array of either, and the pseudo-alias 'number' covers int, long, double and decimal at once.

Clear up null first: a query for null matches both a stored null and a missing field, since both are "no value" to the matcher. Only Piranesi has discount: null, yet all eight documents come back.

null versus missing, and an audit for the wrong type
db.books.countDocuments({ discount: null })                          // stored null OR absent
db.books.countDocuments({ discount: { $exists: false } })            // absent only
db.books.countDocuments({ discount: { $exists: true, $ne: null } })  // present and not null
db.books.find({ price: { $type: ['string', 'int'] } }, { title: 1, price: 1, _id: 0 })
Output
8
7
0
[
  { title: 'The Hobbit', price: 14 },
  { title: 'Snow Crash', price: '11.25' },
  { title: 'Ancillary Justice', price: 15 }
]

The third count is the form you want for an optional field: "this document has a real discount". { $exists: true, $eq: null } is the opposite — explicitly null, which in a well-run schema means something different from absence.

The last query holds two surprises. '11.25' is the string you went looking for; The Hobbit and Ancillary Justice are int because mongosh 403 and the driver serialize a JavaScript number with no fractional part as a 32-bit integer. 14.0 in your source becomes an int on disk while 12.99 stays a double (Numbers, Dates, and Decimal128), so for money store Decimal128 explicitly. The aliases are double, string, object, array, binData, objectId, bool, date, null, regex, int, long and decimal.