Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -2808,13 +2808,14 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate

// Optimization: if primitive path with no validators, or array of primitives
// with no validators, skip validating this path entirely.
if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray) {
if (!_pathType.caster && _pathType.validators.length === 0 && !_pathType.$parentSchemaDocArray && _pathType.instance !== 'Union') {
paths.delete(path);
} else if (_pathType.$isMongooseArray &&
!_pathType.$isMongooseDocumentArray && // Skip document arrays...
!_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
_pathType.validators.length === 0 && // and arrays with top-level validators
_pathType.$embeddedSchemaType.validators.length === 0) {
_pathType.$embeddedSchemaType.validators.length === 0 &&
_pathType.$embeddedSchemaType.instance !== 'Union') {
paths.delete(path);
}
}
Expand Down
152 changes: 152 additions & 0 deletions lib/schema/union.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ class Union extends SchemaType {
throw new Error('Union schema type requires an array of types');
}
this.schemaTypes = options.of.map(obj => options.parentSchema.interpretAsType(key, obj, schemaOptions));

this.validators.push({
validator: () => true,
type: 'union'
});
}

cast(val, doc, init, prev, options) {
let firstValue = firstValueSymbol;
let lastError;
let bestMatch = null;
let bestMatchScore = -1;

const isObject = val != null && typeof val === 'object' && !Array.isArray(val);

// Loop through each schema type in the union. If one of the schematypes returns a value that is `=== val`, then
// use `val`. Otherwise, if one of the schematypes casted successfully, use the first successfully casted value.
// Finally, if none of the schematypes casted successfully, throw the error from the last schema type in the union.
Expand All @@ -36,13 +46,30 @@ class Union extends SchemaType {
if (casted === val) {
return casted;
}

if (isObject && casted != null && typeof casted === 'object') {
const inputKeys = Object.keys(val);
const preservedFields = inputKeys.filter(key => key in casted && key !== '_id').length;
const score = preservedFields;

if (score > bestMatchScore) {
bestMatchScore = score;
bestMatch = casted;
}
}

if (firstValue === firstValueSymbol) {
firstValue = casted;
}
} catch (error) {
lastError = error;
}
}

if (bestMatch !== null) {
return bestMatch;
}

if (firstValue !== firstValueSymbol) {
return firstValue;
}
Expand All @@ -53,6 +80,11 @@ class Union extends SchemaType {
applySetters(val, doc, init, prev, options) {
let firstValue = firstValueSymbol;
let lastError;
let bestMatch = null;
let bestMatchScore = -1;

const isObject = val != null && typeof val === 'object' && !Array.isArray(val);

// Loop through each schema type in the union. If one of the schematypes returns a value that is `=== val`, then
// use `val`. Otherwise, if one of the schematypes casted successfully, use the first successfully casted value.
// Finally, if none of the schematypes casted successfully, throw the error from the last schema type in the union.
Expand All @@ -69,13 +101,31 @@ class Union extends SchemaType {
if (castedVal === val) {
return castedVal;
}


Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Extra blank line should be removed for consistency with the codebase style.

Suggested change

Copilot uses AI. Check for mistakes.
if (isObject && castedVal != null && typeof castedVal === 'object') {
const inputKeys = Object.keys(val);
const preservedFields = inputKeys.filter(key => key in castedVal && key !== '_id').length;
const score = preservedFields;

if (score > bestMatchScore) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going for a best match scoring is a bit overkill IMO, this sort of pattern will be difficult to explain and document. I would prefer to keep more of a "first entry in the union that matches" approach.

bestMatchScore = score;
bestMatch = castedVal;
}
}

if (firstValue === firstValueSymbol) {
firstValue = castedVal;
}
} catch (error) {
lastError = error;
}
}

if (bestMatch !== null) {
return bestMatch;
}

if (firstValue !== firstValueSymbol) {
return firstValue;
}
Expand All @@ -88,6 +138,108 @@ class Union extends SchemaType {
schematype.schemaTypes = this.schemaTypes.map(schemaType => schemaType.clone());
return schematype;
}

/**
* Validates the value against all schema types in the union.
* The value must successfully validate against at least one schema type.
*
* @api private
*/
doValidate(value, fn, scope, options) {
if (options && options.skipSchemaValidators) {
return fn(null);
}

SchemaType.prototype.doValidate.call(this, value, function(error) {
if (error) {
return fn(error);
}
if (value == null) {
return fn(null);
}

if (value && value.schema && value.$__) {
const subdocSchema = value.schema;
for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];
if (schemaType.schema && schemaType.schema === subdocSchema) {
return schemaType.doValidate(value, fn, scope, options);
}
}
}
const validationErrors = [];
let callbackCalled = false;
let completed = 0;

for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];

schemaType.doValidate(value, (err) => {
if (callbackCalled) {
return;
}

completed++;

if (!err) {
callbackCalled = true;
return fn(null);
}

validationErrors.push(err);

if (completed === this.schemaTypes.length) {
callbackCalled = true;
return fn(validationErrors[0]);
}
}, scope, options);
}
}.bind(this), scope, options);
}

/**
* Synchronously validates the value against all schema types in the union.
* The value must successfully validate against at least one schema type.
*
* @api private
*/
doValidateSync(value, scope, options) {
if (!options || !options.skipSchemaValidators) {
const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value, scope);
if (schemaTypeError) {
return schemaTypeError;
}
}

if (value == null) {
return;
}

if (value && value.schema && value.$__) {
const subdocSchema = value.schema;
for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];
if (schemaType.schema && schemaType.schema === subdocSchema) {
return schemaType.doValidateSync(value, scope, options);
}
}
}

const validationErrors = [];

for (let i = 0; i < this.schemaTypes.length; ++i) {
const schemaType = this.schemaTypes[i];

const err = schemaType.doValidateSync(value, scope, options);
if (!err) {
return null;
}

validationErrors.push(err);
}

return validationErrors[0];
}
}

/**
Expand Down
Loading
Loading