Nested data
mongoose-guard walks into every level of the body and checks each nested key against the schema. Allowing a parent field ("address") allows everything inside it. Allowing a child ("address.city") allows only that child. Unknown keys, forbidden keys and wrong types are reported with their full path, such as address.hacked or addresses.1.city.
The examples on this page use this schema:
const addressSchema = new Schema( { city: { type: String, required: true }, zip: { type: String, match: /^\d{6}$/ }, }, { _id: false },);
const userSchema = new Schema({ name: String, profile: { bio: { type: String, maxLength: 160 }, website: String, }, address: addressSchema, addresses: [addressSchema], tags: [String], scores: { type: Map, of: Number }, preferences: Schema.Types.Mixed,});Plain nested objects
Section titled “Plain nested objects”profile above is a plain nested object: Mongoose stores it inline, with no schema of its own.
guard(User, { allow: ["profile"] }); // profile.bio and profile.websiteguard(User, { allow: ["profile.bio"] }); // only profile.bioWith allow: ["profile.bio"], sending { "profile": { "bio": "hi", "website": "a.dev" } } returns forbidden_field at profile.website.
A plain nested object must be an object. "profile": null and "profile": "hi" are both invalid_type.
Sub-schemas
Section titled “Sub-schemas”address is a sub-schema: a separate Schema used as a field type. It behaves the same way for allow lists:
guard(User, { allow: ["address"] }); // address.city and address.zipguard(User, { allow: ["address.city"] }); // only address.cityReal output for { "address": { "city": "Delhi", "hacked": true } } with allow: ["address"]:
{ "ok": false, "issues": [{ "code": "unknown_field", "path": "address.hacked", "message": "Unknown field" }]}And for { "address": { "zip": "12" } } with allow: ["address"] and partial: true:
{ "ok": false, "issues": [ { "code": "invalid_value", "path": "address.zip", "message": "Path `zip` is invalid (12).", "rule": "regexp" } ]}Unlike a plain nested object, a sub-schema may be null when it isn’t required.
Arrays of sub-schemas
Section titled “Arrays of sub-schemas”addresses is a document array. Each element is checked like a sub-schema, and paths include the element’s position:
guard(User, { allow: ["addresses"] }); // every field of every elementguard(User, { allow: ["addresses.city"] }); // only city, in every element| Body | Result |
|---|---|
{ "addresses": [{ "city": "Delhi" }, { "zip": "411001" }] } | invalid_value at addresses.1.city (required) |
{ "addresses": [{ "city": "Pune", "hacked": true }] } | unknown_field at addresses.0.hacked |
{ "addresses": ["Delhi"] } | invalid_type at addresses.0 |
{ "addresses": { "city": "Delhi" } } | invalid_type at addresses (expected array) |
Arrays of primitives
Section titled “Arrays of primitives”tags: [String] holds plain values. Every element’s type is checked:
{ "ok": false, "issues": [{ "code": "invalid_type", "path": "tags.1", "message": "Expected string, received number" }]}That’s the real output for { "tags": ["js", 42] }. Nested arrays such as matrix: [[Number]] work the same way, with paths like matrix.0.1. Array-level validators, such as a custom “at most 5 tags” rule, run too.
You can’t allow a single position ("tags.0"). Allow the whole array.
scores: { type: Map, of: Number } arrives in JSON as a plain object. Each value is checked against the Map’s of type, and paths use the key:
| Body | Result |
|---|---|
{ "scores": { "math": 90 } } | passes |
{ "scores": { "math": "90" } } | invalid_type at scores.math |
{ "scores": { "$where": 1 } } | unsafe_key at scores.$where |
Maps of sub-schemas ({ type: Map, of: addressSchema }) are walked like any other sub-schema, so contacts.home.hacked is reported as unknown.
Map keys can’t start with $ or contain a dot. Mongoose refuses those keys in Maps anyway, and mongoose-guard reports them as unsafe_key before Mongoose gets that far.
Mixed fields
Section titled “Mixed fields”Schema.Types.Mixed accepts any JSON value. mongoose-guard can’t check its shape, but it still scans every key at every level for unsafe keys. Real output for { "preferences": { "filter": { "$where": "sleep(1000)" } } }:
{ "ok": false, "issues": [ { "code": "unsafe_key", "path": "preferences.filter.$where", "message": "Keys starting with $ are not allowed" } ]}To keep this scan cheap, values nested more than 32 levels deep inside a Mixed field are rejected with invalid_body.
Summary
Section titled “Summary”| Field | Example | Allow the whole field | Allow part of it |
|---|---|---|---|
| plain nested object | profile: { bio: String } | "profile" | "profile.bio" |
| sub-schema | address: addressSchema | "address" | "address.city" |
| array of sub-schemas | addresses: [addressSchema] | "addresses" | "addresses.city" |
| array of primitives | tags: [String] | "tags" | not possible |
| Map | scores: { type: Map, of: Number } | "scores" | not possible |
| Mixed | preferences: Mixed | "preferences" | not possible |