MongoDB $replaceRoot Stage

Beginner
⏱️ 11 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

The $replaceRoot stage replaces each document entirely with a new document you specify via newRoot—perfect for promoting nested objects to the top level, flattening after $unwind, or outputting a brand-new document shape built from expressions.

01

newRoot

New document.

02

Promote nested

Lift subdoc.

03

After $unwind

Array flatten.

04

$mergeObjects

Defaults.

05

$ifNull

Missing guard.

06

vs $project

Full swap.

Definition and Usage

$replaceRoot takes each input document and substitutes it with the document produced by newRoot. All original top-level fields—including _id—are discarded unless they appear in the new root.

💡
Beginner tip

Think of it as “make this nested object the whole document”:
Before: { _id: 1, name: "Arlene", pets: { dogs: 2, cats: 1 } }
After newRoot: "$pets": { dogs: 2, cats: 1 }
name and the original _id are gone unless you add them to newRoot.

Common after $lookup or $unwind when you want flat rows instead of wrapped nested structures. Also useful for normalizing inconsistent schemas with $mergeObjects and $$ROOT.

📝 Syntax

$replaceRoot requires a newRoot expression that resolves to a document:

mongosh
{ $replaceRoot: { newRoot: <expression> } }

Syntax Rules

  • newRoot — required. Must evaluate to a document—field path ("$pets"), literal object, or expression like $mergeObjects.
  • Full replacement — the input document is discarded; output is exactly newRoot.
  • Missing field — if newRoot resolves to missing/null, the stage errors. Use $ifNull or $match first.
  • Non-document — strings, numbers, or arrays as newRoot cause an error.
  • $replaceWith — alias-style stage: { $replaceWith: <expression> } (no newRoot wrapper).
  • $$ROOT — reference the current document inside expressions—useful with $mergeObjects.
mongosh
db.people.aggregate([
  {
    $replaceRoot: {
      newRoot: {
        fullName: { $concat: ["$first_name", " ", "$last_name"] },
        city: "$city"
      }
    }
  }
])

⚡ Quick Reference

QuestionAnswer
What it doesReplaces entire document with newRoot expression
Promote nested fieldnewRoot: "$embeddedField"
Build new shapenewRoot: { key: expression, ... }
Handle missing nested$ifNull, $mergeObjects, or $match
After array unwind$unwind then newRoot: "$arrayField"
vs $project$replaceRoot swaps whole doc; $project trims fields
Promote
newRoot:
  "$address"

Lift subdoc

Create new
newRoot: {
  label: "$name"
}

Expression

Defaults
$mergeObjects:
  [defaults, "$x"]

Fill gaps

Fallback
$ifNull:
  ["$x", {}]

No error

🧰 newRoot Expression Patterns

The newRoot value determines the output document. These patterns cover the most common scenarios:

Field path Promote

Replace the document with an embedded subdocument at the given path—simplest promotion pattern.

newRoot: "$pets"
newRoot: "$grades"
Literal object Build

Construct a brand-new document from field paths and aggregation expressions.

newRoot: {
  fullName: { $concat: ["$a", " ", "$b"] },
  _id: "$_id"
}
$mergeObjects Defaults

Merge a default document with a nested field or $$ROOT—fills missing keys without errors.

newRoot: {
  $mergeObjects: [
    { dogs: 0, cats: 0 },
    "$pets"
  ]
}
$ifNull Guard

When some documents lack the nested field, supply a fallback document instead of failing the pipeline.

newRoot: {
  $ifNull: ["$name", { _id: "$_id", missing: true }]
}

Examples Gallery

People, students, and contacts collections—promote embedded docs, unwind arrays, build new roots, and normalize missing fields.

📚 Getting Started

Sample collections for replaceRoot labs.

Example 1 — People, students, and contacts

mongosh
db.people.drop()
db.students.drop()
db.contacts.drop()

db.people.insertMany([
  { _id: 1, name: "Arlene", age: 34, pets: { dogs: 2, cats: 1 } },
  { _id: 2, name: "Sam",    age: 41, pets: { cats: 1, fish: 3 } },
  { _id: 3, name: "Maria",  age: 25 }
])

db.students.insertMany([
  {
    _id: 1,
    studentName: "Alex",
    grades: [
      { test: 1, score: 80, subject: "Math" },
      { test: 2, score: 92, subject: "Math" },
      { test: 3, score: 88, subject: "Science" }
    ]
  },
  {
    _id: 2,
    studentName: "Jordan",
    grades: [
      { test: 1, score: 75, subject: "Math" },
      { test: 2, score: 95, subject: "Science" }
    ]
  }
])

db.contacts.insertMany([
  { _id: 1, first_name: "Gary",  last_name: "Sheffield", city: "New York" },
  { _id: 2, first_name: "Nancy", last_name: "Walker",    city: "Anaheim"  },
  { _id: 3, first_name: "Peter", last_name: "Sumner",    city: "Toledo"   }
])

How It Works

Three collections: embedded pets objects, grades arrays, and flat contact name fields—covering promote, unwind, and create-new patterns.

Example 2 — Promote embedded pets with defaults

mongosh
db.people.aggregate([
  {
    $replaceRoot: {
      newRoot: {
        $mergeObjects: [
          { dogs: 0, cats: 0, birds: 0, fish: 0 },
          { $ifNull: ["$pets", {}] }
        ]
      }
    }
  }
])

// Maria (no pets) → { dogs: 0, cats: 0, birds: 0, fish: 0 }
// Arlene → { dogs: 2, cats: 1, birds: 0, fish: 0 }
// Original name, age, _id removed — only pet counts remain
// $ifNull prevents error when pets is missing

How It Works

$mergeObjects layers defaults under the pets subdocument. $ifNull handles Maria’s missing pets field—avoiding the error you would get from bare newRoot: "$pets".

Example 3 — After $unwind: promote each grade row

mongosh
db.students.aggregate([
  { $unwind: "$grades" },
  { $match: { "grades.score": { $gte: 90 } } },
  {
    $replaceRoot: {
      newRoot: {
        $mergeObjects: [
          "$grades",
          { studentName: "$studentName" }
        ]
      }
    }
  },
  { $sort: { score: -1 } }
])

// Each grade object becomes the root document
// studentName merged in from parent (would be lost with bare "$grades")
// Only scores ≥ 90 kept by $match

How It Works

$unwind creates one document per array element. $replaceRoot lifts grades to root—but merge parent fields you still need, since promotion drops the outer wrapper.

📈 Practical Patterns

Build new documents and normalize schemas.

Example 4 — Create a new root document from expressions

mongosh
db.contacts.aggregate([
  {
    $replaceRoot: {
      newRoot: {
        _id: "$_id",
        full_name: {
          $concat: ["$first_name", " ", "$last_name"]
        },
        location: "$city"
      }
    }
  },
  { $sort: { full_name: 1 } }
])

// Output is ONLY the three fields in newRoot
// first_name and last_name consumed by $concat — not in output
// Clean API-friendly shape from messy source columns

How It Works

When the nested field to promote does not exist, build the root from scratch. Explicitly include _id: "$_id" when you need to preserve the original identifier.

Example 5 — Normalize schema with $$ROOT and defaults

mongosh
db.contacts.aggregate([
  {
    $replaceRoot: {
      newRoot: {
        $mergeObjects: [
          { _id: "", first_name: "", last_name: "", city: "", phone: "" },
          "$$ROOT"
        ]
      }
    }
  },
  {
    $project: {
      _id: 1,
      full_name: { $concat: ["$first_name", " ", "$last_name"] },
      city: 1,
      phone: 1
    }
  }
])

// Default empty strings for missing phone on all contacts
// $$ROOT = entire current document inside newRoot expression
// Then $project for final API shape

How It Works

$$ROOT references the full input document inside newRoot. Merging defaults first ensures every output document has the same keys—useful before writing to a reporting collection.

🧠 How $replaceRoot Works

1

Document arrives

Each document from the prior stage enters $replaceRoot with its current shape.

Input
2

newRoot evaluated

MongoDB evaluates the newRoot expression—path, literal, $mergeObjects, or $ifNull.

Evaluate
3

Document swapped

The input document is discarded; output is exactly the evaluated newRoot document.

Replace
=

New root shape

Downstream stages see flat or normalized documents—ready for $sort, $out, or API responses.

📝 Notes

  • newRoot must resolve to a document—not a string, number, or bare array.
  • Missing nested fields cause a pipeline error unless you guard with $ifNull or $mergeObjects.
  • Original _id is lost unless included in newRoot.
  • $replaceWith is equivalent—see $replaceWith for the alternate syntax.
  • After $unwind, merge parent fields into newRoot if you still need them post-promotion.
  • Previous topic: $redact. Next: $replaceWith.

Conclusion

$replaceRoot is the pipeline’s “swap the whole document” stage: promote nested objects, flatten unwound arrays, or emit a freshly built shape from expressions.

Guard missing fields with $ifNull and normalize with $mergeObjects. Next: $replaceWith for the same behavior with shorter syntax.

💡 Best Practices

✅ Do

  • Use $ifNull or $mergeObjects when nested fields may be absent
  • Explicitly set _id: "$_id" when the new root must keep the original ID
  • Pair with $unwind to explode arrays into flat promoted documents
  • Merge parent context into newRoot after promotion when needed
  • Follow with $project for final field cleanup if the merge step is messy

❌ Don’t

  • Use bare newRoot: "$field" without checking field existence on all docs
  • Expect original top-level fields to survive—they are replaced entirely
  • Use $replaceRoot when $project suffices (simple field trim)
  • Pass non-document values as newRoot—the stage will error
  • Forget that promotion drops parent fields unless merged into newRoot

Key Takeaways

Knowledge Unlocked

Five things to remember about $replaceRoot

Use these points whenever you reshape documents at the root level.

5
Core concepts
📦 02

Promote

"$nested".

Pattern
📈 03

+ $unwind

Flat rows.

Arrays
🛠 04

$ifNull

Safe guard.

Errors
📄 05

vs $project

Whole doc.

Compare

❓ Frequently Asked Questions

$replaceRoot replaces the entire input document with a new document specified by newRoot—an expression that must resolve to a document. Use it to promote an embedded subdocument (e.g. $name) to the top level or to output a freshly computed document shape.
They perform the same operation. $replaceRoot uses { newRoot: expression } while $replaceWith takes the expression directly: { $replaceWith: expression }. Choose whichever form reads clearer in your pipeline.
The original _id is replaced unless you include it in newRoot—e.g. newRoot: "$name" drops the old _id if the nested doc has no _id. Use $mergeObjects or explicitly set _id: "$_id" when you need to preserve it.
The stage errors and fails. Guard with $ifNull (provide a fallback document), $mergeObjects (merge defaults with the nested field), or $match with $exists before $replaceRoot to skip documents without the field.
$project reshapes by including/excluding fields at the current level—the outer document wrapper remains. $replaceRoot discards the entire input document and substitutes newRoot as the new document. Use $replaceRoot to flatten nested objects to root.
When promoting elements from an array—$unwind splits the array into one document per element, then $replaceRoot: { newRoot: "$arrayField" } makes each element the new top-level document. Common for exploding nested arrays into flat rows.
Did you know?

MongoDB added $replaceWith in 4.2 as a shorthand for the same operation—{ $replaceWith: "$pets" } instead of { $replaceRoot: { newRoot: "$pets" } }. Both remain fully supported; pick whichever reads better in your pipeline. See the official $replaceRoot docs.

Continue the Stages Series

Promote nested docs with $replaceRoot, or use the shorthand $replaceWith.

Next: $replaceWith →

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.

5 people found this page helpful