Discriminators give you inheritance over one collection: a base schema, child models that add their own paths and validation, and a key saying which child wrote each document — the polymorphic pattern of Schema Versioning with the bookkeeping done for you.
const itemSchema = new Schema({ title: { type: String, required: true }, year: Number },
{ discriminatorKey: 'kind', collection: 'items' });
const Item = mongoose.model('Item', itemSchema);
const Printed = Item.discriminator('Printed',
new Schema({ pages: { type: Number, required: true } }));
const Audio = Item.discriminator('Audio', new Schema({ narrator: String, minutes: Number }));
await Printed.create({ title: 'Foundation', year: 1951, pages: 255 });
await Audio.create({ title: 'Solaris', year: 1961, narrator: 'A. Juliani', minutes: 462 });
console.log('Item.find ->', (await Item.find()).map((d) => d.kind).join(', '));
console.log('Audio.find ->', (await Audio.find()).map((d) => d.title).join(', '));
console.log('Audio filter ->', JSON.stringify(Audio.find().getFilter()));
try { await Printed.create({ title: 'No pages' }); }
catch (e) { console.log('validation ->', e.message); }Item.find -> Printed, Audio
Audio.find -> Solaris
Audio filter -> {"kind":"Audio"}
validation -> Printed validation failed: pages: Path `pages` is required.Both documents land in items, each carrying kind: 'Printed' or kind: 'Audio'. The base model reads all of them and hydrates each as its own class; a child quietly adds { kind: 'Audio' } to every filter it builds, updates and deletes included, so it can never touch a sibling's documents. Validation is per child: pages is required on Printed and does not exist on Audio.
The trade-offs are the document model's usual ones. One collection means one set of indexes over a mixed population, so a compound index on a child-only path is sparse in practice — put kind first and it becomes selective again. Hooks on the base schema apply to every child, but one registered after discriminator() runs is not inherited, and a document cannot change kind afterwards. Use discriminators when the variants share most of their fields and you query across them, separate models when you do not.