'use strict';
const start = require('./common');
const util = require('./util');
const Ajv = require('ajv');
const assert = require('assert');
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
describe('Union', function() {
let db;
before(async function() {
db = await start().asPromise();
});
after(async function() {
await db.close();
});
afterEach(() => db.deleteModel(/Test/));
afterEach(() => util.clearTestData(db));
afterEach(() => util.stopRemainingOps(db));
it('basic functionality should work', async function() {
const schema = new Schema({
test: {
type: 'Union',
of: [Number, String]
}
});
const TestModel = db.model('Test', schema);
const doc1 = new TestModel({ test: 1 });
assert.strictEqual(doc1.test, 1);
await doc1.save();
const doc1FromDb = await TestModel.collection.findOne({ _id: doc1._id });
assert.strictEqual(doc1FromDb.test, 1);
const doc2 = new TestModel({ test: 'abc' });
assert.strictEqual(doc2.test, 'abc');
await doc2.save();
const doc2FromDb = await TestModel.collection.findOne({ _id: doc2._id });
assert.strictEqual(doc2FromDb.test, 'abc');
});
it('should report last cast error', async function() {
const schema = new Schema({
test: {
type: 'Union',
of: [Number, Boolean]
}
});
const TestModel = db.model('Test', schema);
const doc1 = new TestModel({ test: 'taco tuesday' });
assert.strictEqual(doc1.test, undefined);
await assert.rejects(
doc1.save(),
'ValidationError: test: Cast to Boolean failed for value "taco tuesday" (type string) at path "test" because of "CastError"'
);
});
it('should cast for query', async function() {
const schema = new Schema({
test: {
type: 'Union',
of: [Number, Date]
}
});
const TestModel = db.model('Test', schema);
const doc1 = new TestModel({ test: 1 });
assert.strictEqual(doc1.test, 1);
await doc1.save();
let res = await TestModel.findOne({ test: 1 });
assert.strictEqual(res.test, 1);
res = await TestModel.findOne({ test: '1' });
assert.strictEqual(res.test, 1);
await TestModel.create({ test: new Date('2025-06-01') });
res = await TestModel.findOne({ test: '2025-06-01' });
assert.strictEqual(res.test.valueOf(), new Date('2025-06-01').valueOf());
});
it('should cast updates', async function() {
const schema = new Schema({
test: {
type: 'Union',
of: [Number, Date]
}
});
const TestModel = db.model('Test', schema);
const doc1 = new TestModel({ test: 1 });
assert.strictEqual(doc1.test, 1);
await doc1.save();
let res = await TestModel.findOneAndUpdate({ _id: doc1._id }, { test: '1' }, { returnDocument: 'after' });
assert.strictEqual(res.test, 1);
res = await TestModel.findOneAndUpdate({ _id: doc1._id }, { test: new Date('2025-06-01') }, { returnDocument: 'after' });
assert.strictEqual(res.test.valueOf(), new Date('2025-06-01').valueOf());
});
it('should handle setters', async function() {
const schema = new Schema({
test: {
type: 'Union',
of: [
Number,
{
type: String,
trim: true
}
]
}
});
const TestModel = db.model('Test', schema);
const doc1 = new TestModel({ test: 1 });
assert.strictEqual(doc1.test, 1);
await doc1.save();
const doc2 = new TestModel({ test: ' bbb ' });
assert.strictEqual(doc2.test, 'bbb');
await doc2.save();
const doc2FromDb = await TestModel.collection.findOne({ _id: doc2._id });
assert.strictEqual(doc2FromDb.test, 'bbb');
});
it('handles arrays of unions (gh-15718)', async function() {
const schema = new Schema({
arr: [{
type: 'Union',
of: [Number, Date]
}]
});
const TestModel = db.model('Test', schema);
const numValue = 42;
const dateValue = new Date('2025-06-01');
const doc = new TestModel({
arr: [numValue, dateValue]
});
await doc.save();
const found = await TestModel.collection.findOne({ _id: doc._id });
assert.strictEqual(found.arr.length, 2);
assert.strictEqual(found.arr[0], numValue);
assert.strictEqual(new Date(found.arr[1]).valueOf(), dateValue.valueOf());
});
it('does not bypass validation when a Union of Objects is used (gh-15732)', async function() {
const SubSchema1 = new Schema({
price: { type: Number, required: true },
title: { type: String },
isThisSchema1: { type: Boolean }
}, { _id: false });
const TestSchema = new Schema({
product: {
type: 'Union',
of: [SubSchema1, Number]
}
});
const TestModel = db.model('Test', TestSchema);
const doc = new TestModel({
product: {
title: 'string',
arbitraryNeverSave: true,
isThisSchema1: true,
isThisSchema2: true
}
});
let err;
try {
await doc.validate();
} catch (e) {
err = e;
}
assert.ok(err, 'Expected validation error');
assert.ok(
err.errors['product.price'] || err.errors['product.description'] || err.errors['product'],
'Expected missing required property error'
);
const doc2 = new TestModel({
product: {
price: 42,
title: 'string',
arbitraryNeverSave: true,
isThisSchema1: true,
isThisSchema2: true
}
});
await doc2.validate();
});
it('supports toJSONSchema()', function() {
const schema = new Schema({
test: {
type: 'Union',
of: [Number, String]
},
requiredTest: {
type: 'Union',
required: true,
of: [Boolean, Date]
}
});
assert.deepStrictEqual(schema.toJSONSchema(), {
type: 'object',
required: ['requiredTest', '_id'],
properties: {
test: {
anyOf: [
{ type: 'null' },
{ type: 'number' },
{ type: 'string' }
]
},
requiredTest: {
anyOf: [
{ type: 'boolean' },
{ type: 'string' }
]
},
_id: {
type: 'string',
pattern: '^[A-Fa-f0-9]{24}$'
}
}
});
assert.deepStrictEqual(schema.toJSONSchema({ useBsonType: true }), {
required: ['requiredTest', '_id'],
properties: {
test: {
anyOf: [
{ bsonType: 'null' },
{ bsonType: 'number' },
{ bsonType: 'string' }
]
},
requiredTest: {
anyOf: [
{ bsonType: 'bool' },
{ bsonType: 'date' }
]
},
_id: {
bsonType: 'objectId'
}
}
});
const ajv = new Ajv();
const validate = ajv.compile(schema.toJSONSchema());
assert.ok(validate({ _id: '0'.repeat(24), test: null, requiredTest: true }));
assert.ok(validate({ _id: '0'.repeat(24), test: 42, requiredTest: true }));
assert.ok(validate({ _id: '0'.repeat(24), test: 'answer', requiredTest: '2025-06-01T00:00:00.000Z' }));
assert.ok(!validate({ _id: '0'.repeat(24), test: {}, requiredTest: true }));
const overlappingSchema = new Schema({
test: {
type: 'Union',
of: ['Int32', Number]
}
});
const overlappingValidate = ajv.compile(overlappingSchema.toJSONSchema());
assert.ok(overlappingValidate({ _id: '0'.repeat(24), test: 42 }));
});
});