Allowlisting Field Values with $jsonSchema enum
A field like status, role, type, or currency almost always has a closed vocabulary — a fixed set of values the rest of your system knows how to handle — and the operational question this page answers is how to pin that vocabulary at the database with $jsonSchema enum, sitting under security boundaries in schema design within the broader MongoDB JSON Schema Validation Architecture. The reader outcome is a validator that accepts only the values you have explicitly registered, rejecting an unknown role: "root" or a stray status: "REFUNDED " at the write path with WriteError code 121, so no unregistered category ever reaches a reader that would mishandle it or a privilege check that would trust it.
The design principle is that an allowlist beats a denylist. A denylist enumerates the bad values and fails open on everything it forgot; an enum enumerates the good values and fails closed on everything else — including the value an attacker invents tomorrow. For a privilege field, that difference is the whole security story.
Operational Mechanics and Write-Path Impact
enum constrains a field to an exact set of allowed values. On every insert, update, replace, and findAndModify, the server compares the field’s value against the array; a value not in the array fails the keyword and the write is rejected. Two properties of that comparison matter for a security boundary. First, enum is type-sensitive: the match is by BSON value, so the string "1" does not satisfy an enum of [1, 2, 3], and an integer 1 does not satisfy ["1", "2"]. Second, enum is case- and whitespace-sensitive: "Active", "active", and "active " are three different values, and only an exact member passes.
Because of type sensitivity, enum pairs naturally with bsonType. Declaring bsonType: "string" alongside the enum gives two independent guarantees — the value is a string and it is one of the allowed strings — and produces a cleaner failure signature, because a type-confusion attempt fails on bsonType while an unregistered category fails on enum. Without the bsonType lock, a client sending status: 1 fails only the enum check, which is harder to distinguish from a genuine bad category during triage.
| Field | enum allowlist |
Paired bsonType |
What it stops |
|---|---|---|---|
status |
["open", "settled", "void"] |
string |
Typos, stale states, injected sentinel values |
role |
["viewer", "editor", "owner"] |
string |
Privilege escalation to an unregistered role such as root |
currency |
["USD", "EUR", "GBP"] |
string |
Unsupported currencies that break settlement math |
type |
["card", "bank", "wallet"] |
string |
Unknown payment instruments a processor cannot route |
The most security-relevant use is a privilege field. If role is pinned to an enum, a mass-assignment attempt that sets role: "superadmin" cannot persist, because "superadmin" is not a registered member — the escalation is stopped at the database rather than depending on every application code path to re-check the value. This complements closing the set of fields with an allowlist, covered under preventing field injection with additionalProperties: false; together they close both which keys and which values a document may carry.
One operational consequence follows directly from the closed set: widening an enum is a schema change, not a data change. Adding a new allowed value means editing and redeploying the validator, and until you do, every write carrying the new value fails. That is a feature — it forces a new category to be a reviewed, versioned event — but it means the enum must move in lockstep with producers through your schema versioning strategies for NoSQL.
Exact Diagnostic Fingerprints and Fast Resolution
An enum rejection on MongoDB 5.0+ names the keyword enum in schemaRulesNotSatisfied, reporting what the value was and what the allowed set is. That fingerprint distinguishes a benign typo from an attempted injection and from a legitimate new category the schema has not yet learned:
// Reproduce the fingerprint: an unregistered role is rejected.
db.accounts.insertOne({ account_id: "a-100", role: "superadmin" })
/* Expected: WriteError 121 with errInfo.details.schemaRulesNotSatisfied naming:
{ operatorName: "enum",
specifiedAs: { enum: ["viewer", "editor", "owner"] },
consideredValue: "superadmin" } */
A short pymongo check surfaces the same structure programmatically, so a pipeline can route a rejection by the offending value rather than guessing:
from pymongo import MongoClient
from pymongo.errors import WriteError
client = MongoClient("mongodb://primary:27017")
accounts = client.platform_db.accounts
def create_account(doc: dict) -> str | None:
"""Insert an account, isolating an enum violation on the role field."""
try:
return str(accounts.insert_one(doc).inserted_id)
except WriteError as exc:
if exc.code == 121:
rules = exc.details["errInfo"]["details"]["schemaRulesNotSatisfied"]
for rule in rules:
if rule.get("operatorName") == "enum":
bad = rule.get("consideredValue")
print(f"unregistered value {bad!r} for enum field; not persisting")
return None # do NOT retry — the value is deterministically invalid
raise
# create_account({"account_id": "a-1", "role": "superadmin"}) -> None (rejected)
Match on these signatures to route the incident:
| Signature | Root cause | Resolution |
|---|---|---|
operatorName: "enum" on a privilege field naming a value like root/superadmin |
Escalation attempt via mass assignment | Treat as a security event; forward consideredValue to your SIEM; never widen the enum to accept it |
operatorName: "enum", value differs only in case or trailing space ("Active", "active ") |
Producer emits an inconsistently-cased or padded value | Normalize at the producer (trim/lower) rather than adding the variant to the enum |
operatorName: "bsonType" on an enum field |
Type confusion — a number sent where a string is expected | Fix the producer’s type; the paired bsonType lock caught it before enum |
operatorName: "enum" on a genuinely new, approved category |
The vocabulary legitimately grew | Widen the enum as a versioned schema change, deploy it ahead of the producer |
To find documents already holding an unregistered value, negate the schema with $nor, since $jsonSchema is also a query operator:
db.accounts.countDocuments({ $nor: [ { $jsonSchema: {
bsonType: "object",
properties: { role: { bsonType: "string", enum: ["viewer", "editor", "owner"] } }
} } ] })
// Expected output: 0 on a clean collection.
Step-by-Step Playbook
The safe path registers the vocabulary, proves the data conforms, enforces, then treats every future widening as a versioned change. Values that cannot be normalized should be diverted through fallback routing for invalid documents rather than silently dropped.
1. Register the vocabulary with a paired type lock. Declare each closed-vocabulary field with both bsonType and enum, so a type-confusion attempt and an unregistered value produce distinct fingerprints:
db.runCommand({
collMod: "accounts",
validator: { $jsonSchema: {
bsonType: "object",
required: ["account_id", "role", "status"],
properties: {
account_id: { bsonType: "string", minLength: 1 },
role: { bsonType: "string", enum: ["viewer", "editor", "owner"] },
status: { bsonType: "string", enum: ["active", "suspended", "closed"] }
}
}},
validationLevel: "strict",
validationAction: "warn"
})
// Expected output: { ok: 1 }
2. Prove the existing data conforms. Run the $nor audit for each enum field. It must return 0 before enforcement:
db.accounts.countDocuments({ $nor: [ { $jsonSchema: {
bsonType: "object",
properties: {
role: { enum: ["viewer", "editor", "owner"] },
status: { enum: ["active", "suspended", "closed"] }
}
} } ] })
// Expected output: 0
3. Promote to enforcement. Flip validationAction to error; the change is atomic and metadata-only:
db.runCommand({ collMod: "accounts", validationAction: "error" })
// Expected output: { ok: 1 }
4. Widen the vocabulary as a versioned change. When a new value is genuinely needed, add it to the enum and redeploy the validator before the producer starts emitting it, so the boundary never rejects an approved write:
db.runCommand({ collMod: "accounts", validator: { $jsonSchema: {
bsonType: "object",
required: ["account_id", "role", "status"],
properties: {
account_id: { bsonType: "string", minLength: 1 },
role: { bsonType: "string", enum: ["viewer", "editor", "owner"] },
status: { bsonType: "string", enum: ["active", "suspended", "closed", "dormant"] }
}
}}})
// "dormant" is now a registered member.
Failure Modes & Rollback
Each stage has a distinct failure mode and a bounded recovery:
- Approved value rejected after step 3. A producer emitted a legitimately new category before the enum was widened. This is an ordering failure: widen the enum and deploy the schema first, then the producer — never loosen the field to a free-form string to unblock a release, because that discards the boundary permanently.
- Case/whitespace churn (step 2 count won’t reach zero). Values like
"Active"and"active "register as distinct members and inflate the violation count. Normalize at the producer (trimand case-fold) rather than adding every casing variant to the enum, which would defeat the point of a canonical vocabulary. - Type-confusion mixed with category errors. Without the paired
bsonType, a numericstatus: 1fails onlyenumand is hard to tell from a bad category. Keep the type lock so the two failures stay separable during triage.
Rollback is a single metadata-only collMod. The soft rollback reopens writes while keeping the enum attached for telemetry; the hard rollback detaches it:
// Soft rollback: stop rejecting, keep the vocabulary for logging.
db.runCommand({ collMod: "accounts", validationAction: "warn" })
// Hard rollback: detach the validator entirely (last resort).
db.runCommand({ collMod: "accounts", validator: {}, validationLevel: "off" })
Both take effect on the primary at once and propagate through the oplog, so time-to-recover is the replication lag of your slowest secondary — typically seconds. Prefer the soft rollback; a hard rollback reopens the field to any value, including the escalation values the enum was rejecting.
Frequently Asked Questions
Is $jsonSchema enum matching case-sensitive and type-sensitive?
Yes on both counts. The comparison is an exact BSON value match, so "Active", "active", and "active " are three different members, and the string "1" never satisfies an enum of integers. Register one canonical form per value and normalize at the producer; pair the enum with bsonType so a wrong-typed value fails on the type keyword rather than being confused with a bad category.
Why is an enum allowlist better than a denylist for a privilege field?
A denylist enumerates the forbidden values and fails open on anything it did not anticipate, so a value invented after the rule was written slips through. An enum enumerates the permitted values and fails closed on everything else, so an injected role: "superadmin" is rejected simply because it was never registered. For a field that gates privilege, failing closed is the only safe default.
How do I add a new allowed value without rejecting live writes?
Widening an enum is a versioned schema change. Add the value to the enum array and redeploy the validator with collMod before the producer starts emitting it. Because collMod is metadata-only and atomic, the new member is accepted the instant it lands; deploying it ahead of the producer guarantees no approved write is ever rejected during the rollout.
Related
- Security Boundaries in Schema Design — the parent guide to validators as a persistence-layer security control.
- MongoDB JSON Schema Validation Architecture — the overarching write-path enforcement architecture this allowlist plugs into.
- Preventing Field Injection with additionalProperties: false — closing the set of fields, the complement to closing the set of values.
- Limiting Document Size and Nesting Depth in Validators — bounding resource-exhaustion vectors alongside value allowlisting.
- Schema Versioning Strategies for NoSQL — moving the enum and its producers together when the vocabulary grows.
For the authoritative behavior of enum and the collMod argument surface, consult the official MongoDB schema-validation documentation.