Nested Discriminator with Mongoose
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.

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.
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 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 3: Nested discriminators
What we really want from a block schema is to have some kind of inheritance mechanism. We can have a base block
which will include common properties such as: author
and updated
. The other block models should inherit from the base block
and 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 ofblockSchema
.
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.