Discriminators

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.

One collection, two shapes, separate validationJavaScript
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); }
Output
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.