Using pattern and patternProperties for String Validation

Constraining string values and dynamically-named keys with regular expressions is where most MongoDB validators either become airtight or silently leak, and the deciding factor is almost always a missing anchor. This page sits under Understanding MongoDB $jsonSchema Syntax within the broader MongoDB JSON Schema Validation Architecture, and it gives you the exact behavior of the two regex-driven keywords — pattern, which tests a string value, and patternProperties, which applies a subschema to every property name matching a regex — along with runnable schemas for an email-shaped field, an ISO currency code, and a map of dynamically-named metric keys. The outcome is a validator that rejects malformed strings and undeclared keys deterministically, without the unanchored-regex trap that lets " USDX " slip past a rule you thought demanded three uppercase letters.

Operational Mechanics and Write-Path Impact

Both keywords evaluate on the synchronous write path and both use MongoDB’s PCRE engine, but they act on different targets. pattern is a constraint on a string value: attach it to a property whose bsonType is string, and the value must match the regex. patternProperties is a constraint on property names: its keys are themselves regexes, and any field whose name matches one is validated against the associated subschema. This makes patternProperties the correct tool for dictionary-shaped documents where the keys are data — per-metric counters, per-locale labels, per-currency balances — rather than a fixed field list you could enumerate under properties.

The single rule that governs correctness is anchoring. A PCRE match succeeds if the pattern is found anywhere in the subject, so an unanchored pattern: "[A-Z]{3}" matches "usd-USD-eur" because the substring USD appears inside it. Anchor with ^ and $ to force the regex to describe the entire string: pattern: "^[A-Z]{3}$" accepts exactly USD and rejects usd, US, USDX, and " USD". The same discipline applies to patternProperties keys — an unanchored key regex matches on substring and will claim more field names than you intend, quietly exempting them from an additionalProperties: false closure.

Keyword Applies to Anchored form Accepts Rejects
pattern (currency) string value ^[A-Z]{3}$ USD, EUR usd, US, USDX, " EUR"
pattern (email-shaped) string value ^[^@\s]+@[^@\s]+\.[^@\s]+$ a@b.co a@b, a b@c.d, @b.co
patternProperties (metric map) property name ^metric_[a-z0-9_]+$ key metric_cpu_p95 key Metric_CPU, latency

The performance consequence is real and lands on every write, not just failing ones. Because evaluation is synchronous, an expensive regex is CPU the validator spends before the document is committed. Unanchored patterns are the common culprit: without a ^ anchor the engine may attempt the match at every position in the string, which on long values and high write throughput is measurable latency. Anchored patterns let the engine reject at the first character mismatch. Keep string regexes anchored and bounded, and push genuinely heavy validation to application-layer pre-flight rather than the hot write path, as the collection-level validators workflow describes.

How a property name is routed through properties, patternProperties, and additionalProperties A property key enters the validator and passes three gates left to right. First, if it matches a declared name in properties it is validated against that declared subschema and accepted. If not, it is tested against the patternProperties key regexes; a match validates it against the dynamic-key subschema and accepts it. If it matches neither, additionalProperties set to false rejects the write with code 121. A key is accepted only when properties or patternProperties claims it. A key is accepted only if properties or patternProperties claims it; otherwise additionalProperties:false rejects Property key e.g. metric_cpu In properties? declared name patternProperties? key regex match additionalProperties false → reject · code 121 no no Declared subschema accept Dynamic-key subschema accept yes yes

The interaction with additionalProperties is where these keywords earn their place. When additionalProperties: false is set, a property is permitted only if its name appears in properties or matches a key in patternProperties; anything else is rejected with code 121. That OR is what makes an open-ended metric map expressible without abandoning a closed contract: you declare the fixed fields under properties, describe the dynamic keys under patternProperties, and additionalProperties: false rejects every field that fits neither. Omitting the anchors on a patternProperties key silently widens that allowlist and reopens the door you meant to close.

Exact Diagnostic Fingerprints and Fast Resolution

A pattern violation surfaces as a WriteError with code 121, and on MongoDB 5.0+ the errInfo.details.schemaRulesNotSatisfied array names pattern as the failing operator together with the property path and the value it rejected. This is precise enough to distinguish a genuinely malformed value from an anchoring bug in your own schema:

from pymongo import MongoClient
from pymongo.errors import WriteError

client = MongoClient("mongodb://localhost:27017")
accounts = client.finance.accounts

try:
    accounts.insert_one({"tenant_id": "acme", "currency": "usd"})  # lowercase
except WriteError as exc:
    rules = exc.details["errInfo"]["details"]["schemaRulesNotSatisfied"]
    print(rules)
    # -> operatorName "pattern" on propertyName "currency",
    #    reason "regular expression did not match", consideredValue "usd"

The failure signatures below separate the two most common root causes — a real bad value versus a schema that is too loose because a ^/$ anchor is missing:

Signature Root cause Resolution
code 121, schemaRulesNotSatisfied names pattern on a value you believe is valid The value truly violates the regex (case, length, stray whitespace) Fix the producer, or widen the pattern deliberately if the value is legitimate
A malformed value is accepted under what you think is strict The pattern is unanchored, so a valid substring passed Add ^ and $; re-test the previously-passing bad value
An undeclared field slips past additionalProperties: false A patternProperties key regex is unanchored and claimed the field Anchor the key regex; confirm with the $nor count below
Unknown $jsonSchema keyword at collMod A later-draft string keyword (e.g. format) was used Replace with pattern; MongoDB’s Draft 4 engine does not enforce format

Because $jsonSchema is also a query operator, you can prove an anchoring fix without deploying anything by counting the documents your candidate schema would reject:

// Count values that WOULD fail the anchored currency rule — dry run, no writes.
db.accounts.countDocuments({ $nor: [{ $jsonSchema: {
  bsonType: "object",
  properties: { currency: { bsonType: "string", pattern: "^[A-Z]{3}$" } }
}}] })
// Expected output: an integer — the exact count of non-conforming currency values

Step-by-Step Playbook

This sequence builds and deploys a validator that uses pattern for scalar strings and patternProperties for a dynamic map, proving the anchors hold before enforcement. It follows the observe-then-enforce discipline detailed under strict vs moderate validation levels.

1. Author the schema with anchored patterns and a closed contract. Fixed fields go under properties; the dynamic metric keys go under patternProperties; additionalProperties: false closes everything else:

const schema = { $jsonSchema: {
  bsonType: "object",
  required: ["tenant_id", "currency", "contact_email"],
  properties: {
    tenant_id:     { bsonType: "string", minLength: 1 },
    currency:      { bsonType: "string", pattern: "^[A-Z]{3}$" },
    contact_email: { bsonType: "string", pattern: "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$" }
  },
  patternProperties: {
    "^metric_[a-z0-9_]+$": { bsonType: "double", minimum: 0 }
  },
  additionalProperties: false
}}

2. Verify the anchors on a throwaway collection. Insert a conforming document and a set of near-misses; the near-misses must all be rejected. This is the step that catches a forgotten ^ or $:

db.__pattern_probe.drop()
db.createCollection("__pattern_probe", { validator: schema,
  validationLevel: "strict", validationAction: "error" })

db.__pattern_probe.insertOne({ tenant_id: "acme", currency: "USD",
  contact_email: "ops@acme.io", metric_cpu_p95: 0.42 })          // ok
db.__pattern_probe.insertOne({ tenant_id: "acme", currency: "usd",
  contact_email: "ops@acme.io" })                                 // fails pattern (currency)
db.__pattern_probe.insertOne({ tenant_id: "acme", currency: "USD",
  contact_email: "ops@acme.io", Metric_CPU: 1 })                  // fails additionalProperties
// Expected: first insert ok; second and third throw code 121
db.__pattern_probe.drop()

3. Dry-run the backlog against production data. Before attaching the validator, count how many live documents the anchored rules would reject, using the $nor + $jsonSchema pattern from the previous section, so a strict rollout holds no surprises.

4. Deploy in warn, then promote to error. Attach the schema without rejecting writes, confirm the warning rate is what the dry run predicted, then flip enforcement with a second metadata-only collMod:

db.runCommand({ collMod: "accounts", validator: schema,
  validationLevel: "strict", validationAction: "warn" })
// ...observe, remediate to zero backlog...
db.runCommand({ collMod: "accounts", validationAction: "error" })
// Expected output: { ok: 1 }

Failure Modes & Rollback

Each step fails in a characteristic way, and each has a bounded recovery:

  • Forgotten anchor accepts bad data (step 1–2). The most common defect: an unanchored pattern or patternProperties key silently admits substrings. The probe in step 2 is designed to expose it — if a near-miss is accepted, the regex is unanchored. Fix the ^/$ and re-run the probe before going near production; nothing was deployed, so there is nothing to roll back.
  • Regex escaping mangled by the driver (step 1). In Python and JavaScript source, backslashes must be escaped (\\s, \\.), or the stored pattern differs from what you intended. Read the applied validator back with db.getCollectionInfos({ name: "accounts" })[0].options and confirm the pattern string is exactly the regex you meant.
  • patternProperties over-claims and defeats additionalProperties: false (step 4). An overly broad key regex exempts fields you wanted rejected. Tighten the key regex and re-run the dry-run count; the number of newly-rejected documents tells you how much the loose regex was letting through.

Rollback at any point after step 4 is a single metadata-only collMod that restores write availability without touching data:

// Soft rollback: keep the schema attached for telemetry, stop rejecting.
db.runCommand({ collMod: "accounts", validationAction: "warn" })

// Hard rollback: detach the validator entirely.
db.runCommand({ collMod: "accounts", validator: {}, validationLevel: "off" })

Both take effect on the primary immediately and propagate through the oplog, so time-to-recover is the replication lag of your slowest secondary — typically seconds. Documents that fail a pattern rule under error should be captured rather than dropped; route them through fallback routing for invalid documents so a malformed string becomes a remediation task, not lost data.

Frequently Asked Questions

Why does my pattern accept a string that should fail?

Almost always because the regex is unanchored. MongoDB's PCRE match succeeds if the pattern appears anywhere in the value, so pattern: "[A-Z]{3}" accepts "xxUSDxx" because the substring matches. Wrap the expression in ^ and $"^[A-Z]{3}$" — to require the whole string to conform. Re-test the previously-accepted bad value on a throwaway collection to confirm the fix.

How does patternProperties interact with additionalProperties: false?

A property is allowed when additionalProperties: false is set only if its name appears in properties or matches a key regex in patternProperties; everything else is rejected with code 121. That lets you keep a closed contract while still accepting dynamically-named keys — declare the fixed fields, describe the dynamic ones with an anchored key regex, and the closure rejects the rest. An unanchored key regex silently widens the allowlist, so keep those anchored too.

Can I enforce email or date formats with the format keyword instead of pattern?

No. MongoDB implements JSON Schema Draft 4 and does not enforce format — it is rejected as an unknown keyword at collMod time rather than applied. Use an anchored pattern regex for string shapes such as an email-like field or an ISO currency code, and use bsonType: "date" for real dates rather than trying to pattern-match a date string.

For the authoritative operator behavior referenced above, consult the official MongoDB schema-validation documentation.