'Mongoose: extending schemas

Currently I have two almost identical schemas:

var userSchema = mongoose.Schema({

    email: {type: String, unique: true, required: true, validate: emailValidator},
    passwordHash: {type: String, required: true},

    firstname: {type: String, validate: firstnameValidator},
    lastname: {type: String, validate: lastnameValidator},
    phone: {type: String, validate: phoneValidator},

});

And

var adminSchema = mongoose.Schema({

    email: {type: String, unique: true, required: true, validate: emailValidator},
    passwordHash: {type: String, required: true},

    firstname: {type: String, validate: firstnameValidator, required: true},
    lastname: {type: String, validate: lastnameValidator, required: true},
    phone: {type: String, validate: phoneValidator, required: true},

});

Their only difference is in validation: Users do not need a firstname, lastname or phone. Admins however must have these properties defined.

Unfortunately the above code is not very DRY, as they're almost identical. Therefore I am wondering if it is possible to build an adminSchema based on the userSchema. E.g.:

var adminSchema = mongoose.Schema(userSchema);
adminSchema.change('firstname', {required: true});
adminSchema.change('lastname', {required: true});
adminSchema.change('phone', {required: true});

Obviously that's just pseudocode. Is something like this possible?

Another very similar question is if it is possible to create a new schema based on another, and add some more properties to it. For example:

var adminSchema = mongoose.Schema(userSchema);
    adminSchema.add(adminPower: Number);


Solution 1:[1]

Some people have in other places suggested using utils.inherits to extend schemas. Another simple way would be to simply set up an object with settings and create Schemas from it, like so:

var settings = {
  one: Number
};

new Schema(settings);

settings.two = Number;
new Schema(settings);

It's a bit ugly though, since you're modifying the same object. Also I'd like to be able to extend plugins and methods etc. Thus my preferred method is the following:

function UserSchema (add) {
  var schema = new Schema({
    someField: String
  });

  if(add) {
    schema.add(add);
  }

  return schema;
}

var userSchema = UserSchema();
var adminSchema = UserSchema({
  anotherField: String
});

Which happens to answer your second question that yes, you can add() fields. So to modify some properties of the Schema, a modified version of the above function would solve your problem:

function UserSchema (add, nameAndPhoneIsRequired) {
  var schema = new Schema({
    //...
    firstname: {type: String, validate: firstnameValidator, required: nameAndPhoneIsRequired},
    lastname: {type: String, validate: lastnameValidator, required: nameAndPhoneIsRequired},
    phone: {type: String, validate: phoneValidator, required: nameAndPhoneIsRequired},
  });

  if(add) {
    schema.add(add);
  }

  return schema;
}

Solution 2:[2]

Mongoose 3.8.1 now has support for Discriminators. A sample, from here: http://mongoosejs.com/docs/api.html#model_Model.discriminator

function BaseSchema() {
  Schema.apply(this, arguments);

  this.add({
    name: String,
    createdAt: Date
  });
}
util.inherits(BaseSchema, Schema);

var PersonSchema = new BaseSchema();
var BossSchema = new BaseSchema({ department: String });

var Person = mongoose.model('Person', PersonSchema);
var Boss = Person.discriminator('Boss', BossSchema);

Solution 3:[3]

The simplest way for extending mongoose schema

import { model, Schema } from 'mongoose';

const ParentSchema = new Schema({
  fromParent: Boolean
});

const ChildSchema = new Schema({
  ...ParentSchema.obj,
  fromChild: Boolean // new properties come up here
});

export const Child = model('Child', ChildSchema);

Solution 4:[4]

You can extend the original Schema#obj:

const AdminSchema = new mongoose.Schema({}, Object.assign(UserSchema.obj, {...}))

Example:

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  email: {type: String, unique: true, required: true},
  passwordHash: {type: String, required: true},

  firstname: {type: String},
  lastname: {type: String},
  phone: {type: String}
});

// Extend function
const extend = (Schema, obj) => (
  new mongoose.Schema(
    Object.assign({}, Schema.obj, obj)
  )
);

// Usage:
const AdminUserSchema = extend(UserSchema, {
  firstname: {type: String, required: true},
  lastname: {type: String, required: true},
  phone: {type: String, required: true}
});

const User = mongoose.model('users', UserSchema);
const AdminUser = mongoose.model('admins', AdminUserSchema);

const john = new User({
  email: '[email protected]',
  passwordHash: 'bla-bla-bla',
  firstname: 'John'
});

john.save();

const admin = new AdminUser({
  email: '[email protected]',
  passwordHash: 'bla-bla-bla',
  firstname: 'Henry',
  lastname: 'Hardcore',
  // phone: '+555-5555-55'
});

admin.save();
// Oops! Error 'phone' is required

Or use this npm module with the same approach:

const extendSchema = require('mongoose-extend-schema'); // not 'mongoose-schema-extend'

const UserSchema = new mongoose.Schema({
  firstname: {type: String},
  lastname: {type: String}
});

const ClientSchema = extendSchema(UserSchema, {
  phone: {type: String, required: true}
});

Check the github repo https://github.com/doasync/mongoose-extend-schema

Solution 5:[5]

To add to this discussion, you can also override mongoose.Schema with a custom base schema definition. For code compatibility, add the if statement that allows a Schema to be instantiated without new. While this can be convenient, think twice before doing this in a public package.

var Schema = mongoose.Schema;

var BaseSyncSchema = function(obj, options) {

    if (!(this instanceof BaseSyncSchema))
        return new BaseSyncSchema(obj, options);

    Schema.apply(this, arguments);

    this.methods.update = function() {
        this.updated = new Date();
    };

    this.add({
        updated: Date
    });
};
util.inherits(BaseSyncSchema, Schema);

// Edit!!!
// mongoose.Schema = BaseSyncSchema; <-- Does not work in mongoose 4
// Do this instead:
Object.defineProperty(mongoose, "Schema", {
    value: BaseSyncSchema,
    writable: false
});

Solution 6:[6]

I just published a mongoose-super npm module. Although I did some testing, it is still in an experimental stage. I'm interested to know if it works well for the applications of my fellow SO users!

The module provides a inherit() convenience function that returns a child Mongoose.js model based on a parent model and a child schema extension. It also augments models with a super() method to call parent model methods. I added this functionality because it is something I missed in other extension/inheritance libraries.

The inherit convenience function simply uses the discriminator method.

Solution 7:[7]

All of these answers seem rather needlessly complicated, with extension helper functions or extend methods applied to the schema's or using plugins/discriminators. I've used the following solution instead which is simple, clean and easy to work with. It defines a blueprint for the base schema, and then the actual schema's are built using the blueprint:

foo.blueprint.js

module.exports = {
  schema: {
    foo: String,
    bar: Number,
  },
  methods: {
    fooBar() {
      return 42;
    },
  }
};

foo.schema.js

const {schema, methods} = require('./foo.blueprint');
const {Schema} = require('mongoose');
const FooSchema = new Schema(foo);
Object.assign(FooSchema.methods, methods);
module.exports = FooSchema;

bar.schema.js

const {schema, methods} = require('./foo.blueprint');
const {Schema} = require('mongoose');
const BarSchema = new Schema(Object.assign({}, schema, {
  bar: String,
  baz: Boolean,
}));
Object.assign(BarSchema.methods, methods);
module.exports = BarSchema;

You can use the blueprint for the original schema as is, and using Object.assign you can extend the blueprint in any way you like for other schema's, without modifying the same object.

Solution 8:[8]

You can create a Schema Factory function that accepts a Schema definition and optional schema options, which then merges the passed in Schema definition and options with the Schema fields and options which you want to share across schemas. Example illustrating this (assuming you want to share or extend a schema that has the fields email and is_verified and the timestamps option enabled):

// schemaFactory.js
const mongoose = require('mongoose');

const SchemaFactory = (schemaDefinition, schemaOptions) => {
  return new mongoose.Schema({
    {
      email: {type: String, required: true},
      is_verified: {type: Boolean, default: false},
      // spread/merge passed in schema definition
      ...schemaDefinition
    }
  }, {
    timestamps: true,
    // spread/merge passed in schema options
    ...schemaOptions
  })
}
module.exports = SchemaFactory; 

The SchemaFactory function can then be called with:

// schemas.js
const SchemaFactory = require("./schemaFactory.js")

const UserSchema = SchemaFactory({
  first_name: String,
  password: {type: String, required: true}
});

const AdminSchema = SchemaFactory({
  role: {type: String, required: true}
}, {
  // we can pass in schema options to the Schema Factory
  strict: false
});

Now the UserSchema and AdminSchema will contain both the email and is_verified field as well as have the timestamps option enabled, along with the schema fields and options you pass along.

Solution 9:[9]

I didn't require discrimination, as I was trying to extend sub document schema's which are stored as a part of a parent document anyway.

My solution was to append an "extend" method to the schema that is the base schema, so that you can either use the base schema itself or generate a new schema based on it.

ES6 code follows:

'use strict';

//Dependencies
let Schema = require('mongoose').Schema;

//Schema generator
function extendFooSchema(fields, _id = false) {

  //Extend default fields with given fields
  fields = Object.assign({
    foo: String,
    bar: String,
  }, fields || {});

  //Create schema
  let FooSchema = new Schema(fields, {_id});

  //Add methods/options and whatnot
  FooSchema.methods.bar = function() { ... };

  //Return
  return FooSchema;
}

//Create the base schema now
let FooSchema = extendFooSchema(null, false);

//Expose generator method
FooSchema.extend = extendFooSchema;

//Export schema
module.exports = FooSchema;

You can now use this schema as is, or "extend" it as needed:

let BazSchema = FooSchema.extend({baz: Number});

Extension in this case creates a brand new Schema definition.

Solution 10:[10]

I also try to create the same thing for our project but the above answers and mongoose-extend-schema package fail to accommodate the copy of hooks(methods,virtuals, and query helpers) from the base schema.

To solve this problem i did the following

const BaseSchema = new mongoose.Schema({
  name: String,
  password: {type: String, required: true}
});
const AdminSchemaProperties = {
  name: String,
  password: {type: String, required: true}
};
function extendSchema(oldSchema,properties){
    let cloneSchema = oldSchema.clone()// deep copy the mongoose schema
    let schemaObj = {...cloneSchema.obj,...properties} //create new object with schema object and extended properties
    cloneSchema.obj = schemaObj 
    return cloneSchema
}
const AdminSchema = extendSchema(BaseSchema,AdminSchemaProperties)
export default AdminSchema

Solution 11:[11]

Perhaps this has become easier with later versions of mongoose.

const ToySchema = new Schema();
ToySchema.add({ name: 'string', color: 'string', price: 'number' });

const TurboManSchema = new Schema();
// You can also `add()` another schema and copy over all paths, virtuals,
// getters, setters, indexes, methods, and statics.
TurboManSchema.add(ToySchema).add({ year: Number });

Reference: https://mongoosejs.com/docs/api/schema.html#schema_Schema-add

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Katherine R
Solution 3 nakhodkin
Solution 4
Solution 5
Solution 6 Visionscaper
Solution 7
Solution 8 darthchudi
Solution 9 Adam Reis
Solution 10 mirsahib
Solution 11 DefenestrationDay