MongoDB $redact Stage

Beginner
6 min read
Updated: Oct 2026
3 examples
Live preview

What Is $redact?

$redact is a MongoDB aggregation stage that restricts document content based on an expression. At each level, the expression must resolve to $$KEEP, $$PRUNE, or $$DESCEND.

Remember
$redact = conditional keep / prune / descend

{ $redact: <expression → $$KEEP | $$PRUNE | $$DESCEND> }

$$KEEP    → keep this level; stop inspecting nested content
$$PRUNE   → remove this level entirely
$$DESCEND → keep this level; check nested docs/arrays next

Usually wrapped in $cond
vs $match  → whole document vs nested levels
vs $project → fixed field list vs access rules
UsePrefer
Filter whole documents$match
Fixed include / exclude fields$project
Nested access / tag / level rules$redact

How to Use $redact in MongoDB

Store access hints in the document, write a condition, then map true/false to system variables.

  1. Put access data on each level you care about (for example level or tags).
  2. Optional: $match first to reduce documents.
  3. Add $redact with a $cond that returns $$KEEP, $$PRUNE, or $$DESCEND.
  4. Choose $$DESCEND when nested levels need their own checks; use $$KEEP when the whole subtree is allowed as-is.
PieceRequired?Role
ExpressionYesMust resolve to $$KEEP, $$PRUNE, or $$DESCEND
$condCommonMaps a boolean to those three outcomes
Access fieldUsuallevel, tags, or similar on each level

Pseudocode

Pseudocode
function redact(node):
    decision = evaluate(expression on node)
    if decision is $$PRUNE:
        remove this node
    else if decision is $$KEEP:
        keep this node as-is (no nested checks)
    else if decision is $$DESCEND:
        keep this node's fields
        for each embedded document / array element:
            redact(child)

Cheat Sheet

Keep these facts handy while writing access rules.

TopicDetail
TypeAggregation stage
Syntax{ $redact: <expression> }
$$PRUNEDrop this level (and everything under it)
$$DESCENDKeep level; continue into nested content
$$KEEPKeep level; do not inspect nested content
Level pruneif: { $eq: ["$level", 5] }, then: "$$PRUNE", else: "$$DESCEND"
Tag check$size of $setIntersection of $tags and user tags > 0

Live Preview

Choose a redaction rule and see how a nested account document is trimmed.

Simulates $redact on an account with root level: 1 and nested cc.level: 5.

Result
$redact: if level == 5 → $$PRUNE else $$DESCEND

root level 1 → DESCEND
cc level 5 → PRUNE

output:
  { _id: 1, level: 1, acct_id: "xyz123", status: "A" }

Worked Walkthrough — Prune Level 5

An account has a nested cc object at level: 5. Goal: remove any level equal to 5, keep everything else.

Calculation
Document in:
  {
    _id: 1,
    level: 1,
    acct_id: "xyz123",
    cc: { level: 5, type: "yy", num: "0000" },
    status: "A"
  }

Rule:
  if level == 5 → $$PRUNE
  else → $$DESCEND

root: level 1 → $$DESCEND (keep fields; check children)
cc:   level 5 → $$PRUNE  (drop entire cc)

Document out:
  { _id: 1, level: 1, acct_id: "xyz123", status: "A" }

MongoDB Examples

Insert a sample account, prune by level, then redact by tags. Run these in mongosh in order.

Example 1 — Sample Data

One account with a public root (level: 1) and a sensitive card object (level: 5).

mongosh
db.accounts.insertOne({
  _id: 1,
  level: 1,
  acct_id: "xyz123",
  cc: {
    level: 5,
    type: "yy",
    num: "000000000000"
  },
  status: "A"
});

How the Data Is Stored in MongoDB

After the insert, MongoDB keeps one document in accounts. Run find() to see it:

mongosh
db.accounts.find()

Read the document like this:

FieldExamplePlain English
level1 / 5Access level on this node
acct_id"xyz123"Public account id
ccobjectSensitive card details
status"A"Account status

How It Works

  1. Insert once. insertOne adds the account document.
  2. Levels mark sensitivity. Root is 1; nested cc is 5.
  3. Ready to redact. The next examples apply $redact.

Example 2 — Prune by Level

Goal: drop any level equal to 5, keep other levels and continue into nested content with $$DESCEND.

mongosh
db.accounts.aggregate([
  { $match: { status: "A" } },
  {
    $redact: {
      $cond: {
        if: { $eq: ["$level", 5] },
        then: "$$PRUNE",
        else: "$$DESCEND"
      }
    }
  }
]);

How It Works

  1. Filter first. $match keeps active accounts.
  2. Root check. level is 1, so the result is $$DESCEND.
  3. Nested check. Inside cc, level is 5, so $$PRUNE removes the whole object.
  4. Safe output. Only non-level-5 content remains for the client.

Example 3 — Descend by Tags

Goal: keep a level only when its tags intersect the user’s access list; otherwise prune it.

mongosh
db.forecasts.insertOne({
  _id: 1,
  title: "Department Report",
  tags: ["G", "STLW"],
  year: 2014,
  subsections: [
    {
      subtitle: "Overview",
      tags: ["SI", "G"],
      content: "Public overview."
    },
    {
      subtitle: "Budget",
      tags: ["TK"],
      content: "Restricted budget."
    }
  ]
});

var userAccess = ["STLW", "G"];

db.forecasts.aggregate([
  { $match: { year: 2014 } },
  {
    $redact: {
      $cond: {
        if: {
          $gt: [
            {
              $size: {
                $setIntersection: ["$tags", userAccess]
              }
            },
            0
          ]
        },
        then: "$$DESCEND",
        else: "$$PRUNE"
      }
    }
  }
]);

How It Works

  1. User tags. userAccess is ["STLW", "G"].
  2. Intersection. $setIntersection finds shared tags; $size > 0 means access.
  3. Root and Overview pass. Both share a tag with the user, so $$DESCEND keeps them.
  4. Budget is pruned. Tags ["TK"] do not intersect, so that subsection is removed.

Edge Cases & Pitfalls

Check these before using $redact in production pipelines.

return

Expression must return a system variable

The result must be $$KEEP, $$PRUNE, or $$DESCEND—not true/false alone.

prune

$$PRUNE drops nested content too

Pruning a parent removes children even if a child would have been allowed. Choose $$DESCEND when nested levels need separate checks.

keep

$$KEEP skips nested inspection

If you $$KEEP a parent, nested restricted fields are not re-checked. Use $$DESCEND when children may differ.

missing

Missing tags / level fields

If tags or level is missing, your condition may fail or prune unexpectedly. Design a default branch (often $$PRUNE for safety).

Performance Notes

ApproachCostBest for
$match then $redactFilter firstFewer documents to inspect
$$DESCEND deep treesPer nested levelFine-grained nested security
$$KEEP / early pruneStops recursionShallow decisions when safe

Prefer $match early. $redact with $$DESCEND walks nested structure, so keep documents and nesting as shallow as your security model allows.

Key Takeaways

  • Role: restrict content with $$KEEP, $$PRUNE, or $$DESCEND.
  • $$DESCEND: keep this level and check nested content.
  • Common pattern: $cond on level or tags.
  • vs $match / $project: nested access rules vs whole-doc filter vs fixed field lists.

One line: $redact answers “at this level, keep, prune, or keep looking?”

Frequently Asked Questions

$redact restricts whole documents or nested content based on an expression that resolves to $$KEEP, $$PRUNE, or $$DESCEND. It can evaluate access at each nested level—ideal for tag-based or level-based security.
$$KEEP keeps the current level and stops inspecting nested content. $$PRUNE removes the current level entirely. $$DESCEND keeps the current level but continues evaluating embedded documents and array elements inside it.
$match keeps or drops entire documents. $redact can prune nested subdocuments while keeping the parent—for example, hide a confidential section inside a report. Use $match first to narrow the set, then $redact for fine-grained trimming.
$project shapes fields with fixed include/exclude rules at one level. $redact applies a condition per level and can recurse with $$DESCEND. Use $project for simple column selection; use $redact for role-based or tag-based nested redaction.
Use $$DESCEND when the current level is allowed, but nested subdocuments may have different access rules—for example, a report the user can see, with sections that need separate checks.
Any expression that resolves to $$KEEP, $$PRUNE, or $$DESCEND—most often $cond. Tag checks commonly use $setIntersection with $size; level checks use $eq or comparisons on a level field.

Next: $replaceRoot

Promote a subdocument or expression result to become the new root.

$replaceRoot tutorial →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

11 people found this page helpful