The $redact stage conditionally hides document content based on rules you define—using three system variables ($$KEEP, $$PRUNE, $$DESCEND) to control what users see at each nesting level. It is the go-to stage for tag-based clearance and hierarchical data redaction.
01
$$KEEP
Keep level.
02
$$PRUNE
Remove level.
03
$$DESCEND
Check nested.
04
$cond
If/then/else.
05
Tag access
$setIntersection.
06
vs $match
Nested trim.
Fundamentals
Definition and Usage
$redact evaluates an expression on each document—and on nested subdocuments when you use $$DESCEND—returning one of three outcomes that control visibility at that level.
💡
Beginner tip
Think of three actions at each level: $$KEEP — show this level; don’t inspect children further. $$PRUNE — hide this entire level (and everything inside it). $$DESCEND — show this level; keep checking nested parts. Most pipelines wrap a $cond inside $redact.
Typical pattern: $match to filter the collection, then $redact to enforce per-user or per-role visibility on nested reports, comments, or payment details.
Foundation
📝 Syntax
$redact takes an expression that must resolve to a system variable:
The report has nested subsections each tagged for different audiences. The account embeds payment data at level: 5—ideal for demonstrating $$DESCEND vs $$PRUNE.
$setIntersection finds shared tags between the document and userAccess. $$DESCEND on the parent lets MongoDB apply the same rule to each element in subsections.
Example 3 — Level-based prune (hide payment block)
mongosh
// User clearance level 3 — cannot see level-5 data
var maxVisibleLevel = 3
db.accounts.aggregate([
{ $match: { status: "active" } },
{
$redact: {
$cond: {
if: { $lte: ["$level", maxVisibleLevel] },
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
])
// Top level (level 1) → DESCEND into fields
// payment (level 5) → PRUNE entire payment object
// billing inside payment never evaluated (parent pruned)
// Result: acct_id, status — no card or address data
📤 After redaction:
{ _id: 1, level: 1, acct_id: "acct-1001", status: "active" }
payment field entirely removed
$$PRUNE stops at level-5 node
How It Works
When a node fails the level check, $$PRUNE removes the whole subtree. This is why billing inside payment disappears even though it could theoretically be evaluated separately—parent prune wins.
1 document: Widget Pro
Budget Item and Widget Lite pruned
Flat docs → $$KEEP/$$PRUNE acts like $match
How It Works
On flat documents without nested clearance fields, $$KEEP/$$PRUNE behaves similarly to $match. The power of $redact appears when documents have nested structures with per-level rules.
Example 5 — Why $redact beats $match for nested content
mongosh
// $match: all-or-nothing — drop entire report if ANY rule fails
db.reports.aggregate([
{ $match: { "subsections.tags": "staff" } }
])
// Still returns full report including Financial Detail
// $match only filters TOP-LEVEL docs, not array elements
// $redact: trim individual subsections
var userAccess = ["general", "staff"]
db.reports.aggregate([
{ $match: { year: 2025 } },
{
$redact: {
$cond: {
if: {
$gt: [
{ $size: { $setIntersection: ["$tags", userAccess] } },
0
]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}
])
// Report kept; only unauthorized subsections removed
// $project cannot conditionally drop array elements by tag
// $redact is the right tool for nested conditional visibility
📤 Stage choice:
$match → filter whole documents
$project → field lists, one level
$redact → nested conditional trim
How It Works
$match cannot remove individual array elements based on per-element tags. $project lacks conditional nested traversal. $redact with $$DESCEND fills that gap for hierarchical access control.
Compare
📋 $redact vs related filtering tools
Tool / stage
Effect
Best for
$redact
Conditional keep/prune/descend at each nesting level
Tag clearance, nested reports, hierarchical PII
$match
Include or exclude entire documents
Simple filters before redaction
$project
Include/exclude/rename fields at one level
Static column selection for APIs
Field-level encryption
Encrypt sensitive fields at rest
Storage security, not pipeline shaping
App-layer filtering
Code strips fields after query
Simple cases; duplicates DB logic in app
🧠 How $redact Works
1
Document enters
Each document from the prior stage (often after $match) is evaluated at the root level.
Input
2
Condition tested
The $redact expression runs—typically $cond checking tags, levels, or roles.
Evaluate
3
Action applied
$$KEEP retains, $$PRUNE removes, or $$DESCEND recurses into nested docs and arrays.
Redact
=
🔒
Sanitized output
Clients receive documents with unauthorized nested content removed—not blank entire records.
Important
📝 Notes
The expression inside $redactmust resolve to $$KEEP, $$PRUNE, or $$DESCEND.
$$PRUNE on a parent removes all descendants—children are not evaluated individually.
Use $$DESCEND when nested subdocuments or array elements carry their own tags or level fields.
Define userAccess or clearance variables before the pipeline—never hard-code secrets in production.
$redact is not a substitute for database authentication—combine with proper auth and MongoDB RBAC.
$redact gives pipelines fine-grained control over visibility: keep allowed levels, prune sensitive branches, and descend into nested content when each part has its own access rules.
Start with $match, apply tag or level checks with $setIntersection, and choose the right system variable for each outcome. Next: $replaceRoot to promote nested documents to the top level.
Put $match before $redact to reduce documents processed
Use $$DESCEND for arrays of tagged subsections or comments
Store clearance metadata (tags, level) consistently on every nest level
Test with multiple user role variables to verify prune behavior
Prefer server-side redaction over exposing raw docs to the application
❌ Don’t
Expect $$PRUNE on a parent to expose safe nested fields
Use $redact as your only security layer—enforce auth at the database
Hard-code production role arrays in shared pipeline code without parameterization
Assume $match on array fields trims individual elements—it does not
Forget that $$KEEP skips nested re-evaluation—unsafe if children need checks
Summary
Key Takeaways
Knowledge Unlocked
Five things to remember about $redact
Use these points whenever you implement conditional document visibility.
5
Core concepts
🔒01
Three actions
KEEP/PRUNE/DESCEND.
Basics
🔎02
Nested trim
Not all-or-nothing.
Power
🏷03
Tag check
$setIntersection.
Pattern
🚫04
$$PRUNE
Whole branch gone.
Rule
📈05
vs $match
Per-level control.
Compare
❓ Frequently Asked Questions
$redact conditionally includes or excludes document content based on an expression that resolves to $$KEEP, $$PRUNE, or $$DESCEND. Unlike $match (all-or-nothing per document) or $project (field lists), $redact can evaluate access at each nested level—ideal for hierarchical or tag-based security.
$$KEEP keeps the current level and stops further inspection at that level. $$PRUNE removes the current level entirely without checking nested content. $$DESCEND keeps the current level but continues evaluating embedded documents and array elements inside it.
$match drops or keeps entire documents based on a filter. $redact can prune nested subdocuments while keeping the parent—e.g. hide one confidential section inside a report but keep other sections. Use $match first to narrow the set, then $redact for fine-grained trimming.
$project shapes fields at one document level with fixed include/exclude rules. $redact applies conditional logic per level and can recurse into nested structures with $$DESCEND. Use $project for simple column selection; use $redact for role-based or tag-based nested redaction.
Use $$DESCEND when the current level passes the access check but nested subdocuments or array elements may have different access rules—e.g. a report the user can see, but individual sections tagged with different clearance levels inside subsections.
Any expression resolving to $$KEEP, $$PRUNE, or $$DESCEND—most commonly $cond: { if: condition, then: "$$DESCEND", else: "$$PRUNE" }. Tag checks often use $setIntersection with $size; level checks use $eq or $lte on a level field.
Did you know?
MongoDB’s classic forecasts example in the official docs uses exactly this pattern: $$DESCEND when user tags intersect document tags, $$PRUNE otherwise—allowing a user to read a report while individual subsections with higher clearance are stripped. See the official $redact docs.