Nested Discriminator with Mongoose

Doruk Güneş
3 min readOct 17, 2021

I really prefer learning something new with a real-world example rather than some generic “foo bar” example. Let’s try to understand nested discriminators in mongoose with a real-world example and let’s also discover some other alternatives over the discriminator usage.

Imagine building the new “Notion” and you try to figure out database modeling for the “Page” which consists of blocks/elements with different types and functionalities such as Emoji, Todo list, Divider, Link to page, and so on.

example page from Notion

You decided to create a collection named pages to store each page document separately and store an array of blocks under the Page model. We will cover why we decided to embed the blocks under the Page model in a different post.

example mongoose Page model

So what type of blocks and properties should we store under the blocks array?

  • The Heading: requires a text field property.
  • Todo List: requires an array of nested todos.
  • Divider: nothing specific to store.
  • Link to page: requires the reference “_id” to the Page model.

Besides the specific needs of the specific block types, we also need to store some common properties for all the block types such as _id, author, created, and updated dates.

What alternatives do we have to create this “blocks” schema?

Alternative 1: Make the “blocks” schema type Mixed

If we change the schema type to Mixed we will lose all the mongoose goodness such as type casting and validation hooks.

alternative 1 mongoose model example

Alternative 2: Make a single “blocks” schema

One other alternative is to store all the required properties under the blocks schema with this approach we have to define the common properties for all the blocks such as author and updated to be required and other fields should not be required in any case. This is a downside of this approach since for example, what we really want is to referancePage be required when the type is link-to-page.

It is also really hard to understand which field is required in which block type and can easily become more complex when you will introduce new block types.

alternative 2 mongoose model example

Alternative 3: Nested discriminators

What we really want from a block schema is to have some kind of inheritance mechanism. We can have a base blockwhich will include common properties such as: author and updated. The other block models should inherit from the base blockand implement their own properties and functions on top of it.

Discriminators are the way to achieve this functionality with mongoose.

Let’s go over what we defined there:

  • We defined a blockSchema which includes the common properties and we will use this schema to define inheritance.
  • We defined other block schemas and they only have their own properties.
  • We defined the pageSchema and it has an array of blockSchema.

As you may have noticed we gave a { discriminatorKey: “type” } to the block schema. If you don’t give this option mongoose will use default discrimonator key “__t”

Now we have to tell mongoose to use discriminator for the blocks field.

First, we use the `path` function on pageSchema to select the schema type of blocks, then we use this schema and call discriminator function which takes the discriminatorKey as the first parameter and the schema as the second.

Here is an example of how we create a new page document with different types of blocks.

Here is another example where we also want to fetch and populate the referancePage of the link to page block.

Advantages of using this approach:

  • A clear schema definition for each type
  • Mongoose validation and hooks
  • Easy population

If you are using mongoose as ORM and you have a challenge when you want to apply polymorphism inside a single collection “discriminators” are a nice feature that you should consider.

--

--