BlogNode.js

Using MongoDB project to change a field name (without aggregation)

Written by Codemzy on January 24th, 2024

If your MongoDB data doesn't match up with the response object you need to provide, you can use `.project()` to change a field name and respond with a different field name instead!

Sometimes you might need to change a field name when you get data from your MongoDB database.

If you don't want to change the document stored within the database (maybe because you need those other details for other uses), and you just want to change the field name for what is returned from the database, you can use .project() to change the field name.

This could be the case if you want to pull certain fields for different queries, or if you have a mix of different documents and you need to make them all match the same structure in the response.

In this blog post, we will look at how you can:

  • Use project to give a field a different name (simple example)
  • Use $ifNull to only replace a field if it doesn't exist (more complex)
  • Use cond with project to change a field name (most complex)

In all of the examples, we are not going to be changing the document that is stored within the database. We will just be changing the response that's returned from the database. That's why we are using .project().

You can also use the examples with the $project aggregation stage instead of the .project() chained method, but in this blog post we will be using .find().

How to use project to give a field a different name

Let's imagine you have a users collection in your database, and each user is structured like this:

{
  _id: 1,
  created: 1706031771000,
  details: {
    name: {
      first: "Doug",
      last: "Heffernan"
    }
    address: {
      // ...
    }
  }
}

The dashboard of your website uses this collection to get the first name of the user and display it.

Like "Hi Doug!".

This is an example of when it might be easier - or make more sense - to access a field under a different name. Your front end only needs the first name and expects user.firstName instead of user.details.name.first.

I'm going to stick with name.first vs firstName example throughout this post, but you can apply the code to any fields you need to change in your projects.

So let's say you decide you want to change what is returned from the database to:

{
  _id: 1,
  created: 1706031771000,
  firstName: "Doug",
}

We can use .project() to do this! Instead of giving the field a value of 0 or 1 (to exclude or include it), we're going to use an aggregation expression.

Now you might be thinking aggregation expressions are only available in aggregation pipelines. And that was true before MongoDB 4.4. But luckily for us, as of version 4.4, we can use aggregation expressions and syntax in find() and findAndModify() projections.

This means we can use projection to tell MongoDB to return a field that doesn't even exist in our document and assign it to an existing field value.

Like this:

{ firstName: "$details.name.first"}

We know the firstName field doesn't exist, so we assign it to the $details.name.first field.

Here's our user document again:

{
  _id: 1,
  created: 1706031771000,
  details: {
    name: {
      first: "Doug",
      last: "Heffernan"
    }
    address: {
      // ...
    }
  }
}

And here's our find:

// get all users
const cursor = db
  .collection('users')
  .find({})
  .project({ created: 1, firstName: "$details.name.first" });

// get a single user
let user = await db.collection('users').findOne({ _id: 1 }, { 
    projection: { created: 1, firstName: "$details.name.first" } 
  } 
});

That's a simple way to project a field with a different name. Let's dive into some more complex examples.

How to use $ifNull to only replace a field if it doesn't exist

Ok, let's imagine you have updated your app, and you decided that you should change the structure of users in the users collection. user.firstName is wayyyyy more readable and easy to remember than user.details.name.first and you're sick of all those dots.

So you're going to rename the fields in your database for real.

{
  _id: 1,
  created: 1706031771000,
  firstName: "Doug",
  lastName: "Heffernan",
  address: {
    // ...
  }
}

Much neater!

But there's a problem.

Some users might still be loading the old version of your website. And that will still expect user.details.name.first. Eek!

Should you start duplicating data on your user objects? So that it works for the old version of your app and the new version until people reload the latest version?

{
  _id: 1,
  created: 1706031771000,
  firstName: "Doug",
  lastName: "Heffernan",
  address: {
    // ...
  },
  details: { // old
    name: { // old
      first: "Doug", // old
      last: "Heffernan" // old
    } // old
    address: { // old
      // ... // old
    } // old
  } // old
}

That's pretty messy with all that duplicate data.

Instead, we can fix this with .project()! For any API requests that come in for version 1 of the API, we need to respond as if the database is the same as it was before.

We can create the field details.name.first and assign it to the new firstName field (the opposite of what we did earlier), and that would work - but the problem is, we're planning to update the database in a few weeks, not today. And we don't want to break the API in the meantime.

If we did this { details.name.first: "$firstName"} today, it wouldn't work until there is a firstName field when we update the database in a few weeks.

Luckily for us, we can use $ifNull.

{ "details.name.first": { $ifNull: [ "$details.name.first", "$firstName" ] } }

It's kind of like giving us a backup field. If details.name.first is null, then it tells MongoDB to get the value from the firstName field instead!

Here's how it looks:

// get all users
const cursor = db
  .collection('users')
  .find({})
  .project({ 
    created: 1, 
    "details.name.first": { $ifNull: [ "$details.name.first", "$firstName" ] }
  });

// get a single user
let user = await db.collection('users').findOne({ _id: 1 }, { 
    projection: { 
      created: 1, 
      "details.name.first": { $ifNull: [ "$details.name.first", "$firstName" ] } 
    } 
  } 
});

How to use cond with project to change a field name

Ok, let's try something a little more tricky. I'm storing the addresses of my users, and I'm running a special offer for all my UK users. I want to display it on the dashboard when they sign in.

{
  _id: 2,
  created: 1706117719000,
  details: {
    name: {
      first: "Hyacinth",
      last: "Bucket"
    }
    address: {
      // ...
      countryCode: "UK"
    }
  }
}

I don't need their full address, or even what country they are - I just need to know if they are in the UK.

It would be pretty cool to have an isUK field for that. But, I don't! I could respond with the country code, and let the front end do the work, or I could change the name of the field to create an isUK field and change the value to true or false.

Let's do that!

We can use $cond to create the new field and have an if-then-else argument.

{ 
  isUK: {
    $cond: { 
      if: { $eq: [ "$details.address.countryCode", "UK" ] }, 
      then: true, 
      else: false 
    }
  }
}

We're not really changing a field name here, since we are also changing the value, but it shows how you return data under a field with a different name, and manipulate the value too, for more complex use cases!

// get all users
const cursor = db
  .collection('users')
  .find({})
  .project({ 
    created: 1, 
    firstName: "$details.name.first",
    isUK: {
      $cond: { 
        if: { $eq: [ "$details.address.countryCode", "UK" ] }, 
        then: true, 
        else: false 
      }
    }
  });

// get a single user
let user = await db.collection('users').findOne({ _id: 2 }, { 
    projection: { 
      created: 1,
      firstName: "$details.name.first",
      isUK: {
        $cond: { 
          if: { $eq: [ "$details.address.countryCode", "UK" ] }, 
          then: true, 
          else: false 
        }
      }
    } 
  } 
});