skip to content

How does $addFields differ from $project, and when do you need $project instead?

level: middleimportance: should knowfreq 55%

answer

  1. one merges, the other rebuilds
  2. unlisted fields survive in only one of them
  3. truthy values put a stage into inclusion mode
  4. only _id may be excluded alongside inclusions
  5. the final shape stage is the allowlist

basics

~20 s

$addFields, and its alias $set, adds or overwrites the fields you name while keeping every other field. $project builds a new shape — an inclusion projection keeps only the fields you list — so use $project when you need to drop or rename fields.

solid answer

~50 s

`$addFields` is additive: it evaluates the expressions you give it and merges them into the incoming document, leaving all other fields untouched. Naming an existing field overwrites it, and dotted paths let you set a field inside a sub-document. `$set` is a straight alias for it, introduced so pipelines read like update syntax. `$project` is a **reshaping** stage: as soon as you list a field with a truthy value you are in inclusion mode, and only `_id` plus the fields you listed survive. That is what makes `$project` the tool for dropping fields, renaming (`{ customer: "$customerId" }`), and building a completely new document shape for an API response. You cannot mix inclusions and exclusions in one `$project`, except for suppressing `_id`. An exclusion-only `$project` is legal, and `$unset` is the shorthand alias for it.

code

javascript · 6 lines
javascript
// input: { _id: 1, sku: "A", price: 10, qty: 3, notes: "bulky" }
{ $addFields: { total: { $multiply: ["$price", "$qty"] } } }
// -> { _id: 1, sku: "A", price: 10, qty: 3, notes: "bulky", total: 30 }

{ $project: { total: { $multiply: ["$price", "$qty"] } } }
// -> { _id: 1, total: 30 }

go deeper

for a junior

Recall the one-line difference: $addFields keeps everything and adds, $project keeps only what you list. Know that $set is another name for $addFields.

for a middle

Explain inclusion versus exclusion mode, why the two cannot be mixed apart from _id, and how renaming works with a dollar-prefixed source path. Be able to predict the output shape of either stage.

for a senior

Argue the practical consequences: projecting early to shrink the documents later stages carry, and using a closing inclusion projection as an allowlist so newly added internal fields never leak into an API response.

for a principal

Own the convention across a codebase — where in every pipeline the response shape is pinned, and whether read models are shaped in the database or in the service layer. Consistency here is what keeps field leakage from recurring.

## Two stages that both compute fields Both `$addFields` and `$project` evaluate aggregation expressions and put the results into the output document. The difference is what happens to everything you did *not* mention. `$addFields` merges. The output is the input document plus your new fields. Nothing is lost. `$project` replaces. The output is a document built from your specification alone. In inclusion mode, a field you did not list is gone. ```javascript // input: { _id: 1, sku: "A", price: 10, qty: 3, notes: "..." } { $addFields: { total: { $multiply: ["$price", "$qty"] } } } // -> { _id: 1, sku: "A", price: 10, qty: 3, notes: "...", total: 30 } { $project: { total: { $multiply: ["$price", "$qty"] } } } // -> { _id: 1, total: 30 } ``` That single difference decides which one you reach for. Computing an intermediate value that later stages need, while the rest of the document stays intact? `$addFields`. Producing the final shape a client will receive? `$project`. ## $set is the same stage `$set` is an alias for `$addFields`, added so that pipeline syntax mirrors the update-operator syntax people already know. There is no behavioural difference; pick one and be consistent within a codebase. Symmetrically, `$unset` is an alias for an exclusion-only `$project`: `{ $unset: ["notes", "internal"] }` and `{ $project: { notes: 0, internal: 0 } }` do the same thing. ## Inclusion and exclusion modes `$project` operates in one of two modes, decided by the values you supply: - **Inclusion mode** — any field set to `1`, `true`, or an expression. Only the listed fields (plus `_id`) come through. - **Exclusion mode** — every field set to `0` or `false`. Everything comes through except the listed fields. Mixing the two in one stage is an error, with exactly one exemption: you may write `_id: 0` alongside inclusions, because `_id` is included by default and suppressing it is the common case. So `{ $project: { sku: 1, price: 1, _id: 0 } }` is fine, while `{ $project: { sku: 1, notes: 0 } }` is rejected. ## Overwriting and nested paths Both stages accept dotted paths and both will overwrite. `{ $set: { "address.country": "US" } }` replaces just that sub-field and leaves the rest of `address` alone; assigning `{ $set: { address: { country: "US" } } }` replaces the whole sub-document. Overwriting a field with a computed version of itself is idiomatic — `{ $set: { price: { $round: ["$price", 2] } } }` — and is exactly the kind of thing `$project` makes awkward, since you would have to re-list every other field you wanted to keep. ## Renaming Renaming is `$project` (or `$set` + `$unset`) territory: `{ $project: { customer: "$customerId", total: 1, _id: 0 } }` emits `customer` instead of `customerId`. Note the dollar prefix — `{ customer: "customerId" }` would set the literal string. Renaming is also how you flatten a `$group` result, whose useful key is buried in `_id`: `{ $set: { region: "$_id.region" } }` followed by `{ $unset: "_id" }`. ## Why the choice matters beyond style Three practical consequences: **Document size in the stream.** Every stage after this one carries whatever the document holds. If a collection has a large `body` or `attachments` field that the report never touches, projecting it away early makes every subsequent sort, group and network hop cheaper. `$addFields` never shrinks anything. **Accidental leakage.** An API endpoint built on `$addFields` returns whatever happened to be in the document, including fields added later by some other part of the system. An inclusion `$project` at the end of the pipeline is an allowlist, and that is a security-relevant property: new internal fields do not silently appear in responses. **Breakage when the schema drifts.** An inclusion `$project` that omits a newly important field is an obvious, visible bug. An `$addFields` pipeline quietly passes through everything, which is more forgiving but also means you cannot read the output shape from the pipeline text. ## Rule of thumb Use `$set`/`$addFields` in the middle of a pipeline to compute what later stages need. Use `$project` at the end to state exactly what leaves. If you find yourself writing a `$project` that lists twelve fields with `1` just to add a thirteenth computed one, that is the signal you wanted `$addFields`.

  • Why does { $project: { sku: 1, notes: 0 } } fail?
    A `$project` is either an inclusion or an exclusion specification, never both. Listing `sku: 1` puts the stage in inclusion mode, where every unlisted field is already dropped, so `notes: 0` is contradictory and MongoDB rejects it. The one exemption is `_id: 0`, which may accompany inclusions because `_id` is otherwise included by default.
  • How do you rename a field in an aggregation pipeline?
    Assign the old field's path to the new name: `{ $project: { customer: "$customerId", _id: 0 } }`, or `{ $set: { customer: "$customerId" } }` followed by `{ $unset: "customerId" }` if you want to keep the rest of the document. The dollar prefix is essential — without it you assign the literal string `"customerId"`.
  • Is there a reason to prefer $project even when you are only adding a field?
    Yes, when the pipeline output goes to a client. An inclusion `$project` is an explicit allowlist, so fields added to the collection later never leak into responses, and dropping large untouched fields early makes every downstream stage carry less data. For purely internal intermediate values, `$set` is clearer.

saying these in an interview costs you the question

  • Thinks $project keeps unlisted fields the way $addFields does
  • Believes $set and $addFields behave differently
  • Tries to mix inclusions and exclusions in one $project
  • Writes { customer: "customerId" } and expects a rename
  • Assumes $addFields modifies the stored document

context