Skip to content

How validation works

Every request body goes through five checks, in this order: the body must be a plain object, it must not contain unsafe keys, every key must exist in the schema and be allowed on this route, every value must have exactly the right type, and finally the Mongoose rules in your schema must pass. If any of the first four checks fail, mongoose-guard returns those issues straight away and does not run the Mongoose rules.

body
│
├─ 1. Is it a plain object? invalid_body
├─ 2. Any unsafe keys? unsafe_key
├─ 3. Is every key in the schema and allowed? unknown_field, forbidden_field
├─ 4. Is every value the right type? invalid_type
│ (stop here if anything above failed)
└─ 5. Do the schema's rules pass? invalid_value

The body has to be a JSON object: { ... }. Arrays, strings, numbers, null and a missing body all fail.

{ "code": "invalid_body", "path": "", "message": "Expected a JSON object" }

In Express, the usual cause of a missing body is forgetting app.use(express.json()).

Some keys are never accepted, anywhere in the body:

  • __proto__, constructor and prototype, which are used in prototype pollution attacks
  • keys starting with $, such as $where, $gt or $set, which are MongoDB operators
  • keys containing a dot, such as "address.city", which MongoDB treats as a path into a nested object

This check goes all the way down, including inside Mixed fields and Map keys. It also applies in strip mode: unsafe keys are always errors, never silently removed. The security page explains why.

Each key is looked up in the schema.

  • Not in the schema at all: unknown_field. Usually a typo like nmae, or a client sending fields from a newer API version.
  • In the schema, but not in this route’s allow list: forbidden_field. This is the mass-assignment check.

In strip mode, both of these are removed quietly instead of being reported.

A Number field needs a real JSON number. A String field needs a string. Nothing is converted. "25" for a Number is an invalid_type issue, even though Mongoose would happily cast it.

This also walks into nested objects, sub-schemas, arrays and Maps, so tags: ["js", 42] reports tags.1. The full list of rules is in Type rules.

Only when the first four checks are clean does mongoose-guard ask Mongoose to validate. This is where required, min, max, minLength, maxLength, enum, match and your custom validators run, including async ones.

Mongoose runs these rules itself. mongoose-guard doesn’t reimplement them, so they behave exactly as they do when you call save().

Running Mongoose rules on a body with the wrong shape produces noise. If age is the string "abc", a min: 13 check has nothing sensible to say. If there’s a forbidden role field, there’s no point checking whether its value is in the enum. Reporting the structural problems first gives the client one clear thing to fix.

It also saves work. An async validator that queries the database never runs on a request that was going to fail anyway.

That depends on partial:

ModeFields Mongoose validates
default (partial: false)every field that was sent, plus every allowed field that wasn’t sent, so required works
partial: trueonly the fields that were sent

Fields that are not in the allow list are never validated. A required field like createdBy, which your server fills in, won’t fail just because the client can’t send it. More on this in Partial updates.