From 41bcccfbc227b40d01a63a2cdda36f0368c4eaf0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 27 Oct 2025 14:30:45 -0400 Subject: [PATCH 1/3] refactor: pass parent schema to SchemaType constructors in interpretAsType to make implementing custom container types easier --- lib/schema.js | 5 +---- lib/schema/union.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 6ec1c37f99d..c2ab4a5b8ba 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1739,10 +1739,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { 'https://bit.ly/mongoose-schematypes for a list of valid schema types.'); } - if (name === 'Union') { - obj.parentSchema = this; - } - const schemaType = new MongooseTypes[name](path, obj, options); + const schemaType = new MongooseTypes[name](path, obj, options, this); if (schemaType.$isSchemaMap) { createMapNestedSchemaType(this, schemaType, path, obj, options); diff --git a/lib/schema/union.js b/lib/schema/union.js index b99194d25a0..e9f8e5b573a 100644 --- a/lib/schema/union.js +++ b/lib/schema/union.js @@ -14,12 +14,12 @@ const firstValueSymbol = Symbol('firstValue'); */ class Union extends SchemaType { - constructor(key, options, schemaOptions = {}) { + constructor(key, options, schemaOptions, parentSchema) { super(key, options, 'Union'); if (!options || !Array.isArray(options.of) || options.of.length === 0) { throw new Error('Union schema type requires an array of types'); } - this.schemaTypes = options.of.map(obj => options.parentSchema.interpretAsType(key, obj, schemaOptions)); + this.schemaTypes = options.of.map(obj => parentSchema.interpretAsType(key, obj, schemaOptions)); } cast(val, doc, init, prev, options) { From d7d2789e4ee1b5e27c7ec4762d052216ee932033 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Nov 2025 16:12:25 -0500 Subject: [PATCH 2/3] refactor: handle passing parentSchema correctly to all schema types --- .../discriminator/mergeDiscriminatorSchema.js | 10 ++++++ lib/schema.js | 18 +++++----- lib/schema/array.js | 9 ++--- lib/schema/bigint.js | 6 ++-- lib/schema/boolean.js | 6 ++-- lib/schema/buffer.js | 6 ++-- lib/schema/date.js | 6 ++-- lib/schema/decimal128.js | 6 ++-- lib/schema/documentArray.js | 26 +++++++++++---- lib/schema/documentArrayElement.js | 6 ++-- lib/schema/double.js | 6 ++-- lib/schema/int32.js | 6 ++-- lib/schema/map.js | 4 +-- lib/schema/mixed.js | 6 ++-- lib/schema/number.js | 6 ++-- lib/schema/objectId.js | 6 ++-- lib/schema/string.js | 6 ++-- lib/schema/subdocument.js | 8 +++-- lib/schema/union.js | 10 +++++- lib/schema/uuid.js | 6 ++-- lib/schemaType.js | 33 +++++++++++++++++-- 21 files changed, 142 insertions(+), 54 deletions(-) diff --git a/lib/helpers/discriminator/mergeDiscriminatorSchema.js b/lib/helpers/discriminator/mergeDiscriminatorSchema.js index b48d5c4db07..aae99068d15 100644 --- a/lib/helpers/discriminator/mergeDiscriminatorSchema.js +++ b/lib/helpers/discriminator/mergeDiscriminatorSchema.js @@ -3,6 +3,7 @@ const schemaMerge = require('../schema/merge'); const specialProperties = require('../../helpers/specialProperties'); const isBsonType = require('../../helpers/isBsonType'); const ObjectId = require('../../types/objectid'); +const SchemaType = require('../../schemaType'); const isObject = require('../../helpers/isObject'); /** * Merges `from` into `to` without overwriting existing properties. @@ -69,6 +70,15 @@ module.exports = function mergeDiscriminatorSchema(to, from, path, seen) { } else if (isBsonType(from[key], 'ObjectId')) { to[key] = new ObjectId(from[key]); continue; + } else if (from[key] instanceof SchemaType) { + if (to[key] == null) { + to[key] = from[key].clone(); + } + // For container types with nested schemas, we need to continue to the + // recursive merge below to properly merge the nested schemas + if (!from[key].$isMongooseDocumentArray && !from[key].$isSingleNested) { + continue; + } } } mergeDiscriminatorSchema(to[key], from[key], path ? path + '.' + key : key, seen); diff --git a/lib/schema.js b/lib/schema.js index c2ab4a5b8ba..c0f6e8fec7d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1572,7 +1572,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { let name; if (utils.isPOJO(type) || type === 'mixed') { - return new MongooseTypes.Mixed(path, obj); + return new MongooseTypes.Mixed(path, obj, null, this); } if (Array.isArray(type) || type === Array || type === 'array' || type === MongooseTypes.Array) { @@ -1595,7 +1595,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { `${path}: new Schema(...)`); } } - return new MongooseTypes.DocumentArray(path, cast, obj); + return new MongooseTypes.DocumentArray(path, cast, obj, null, this); } if (cast && cast[options.typeKey] && @@ -1612,14 +1612,14 @@ Schema.prototype.interpretAsType = function(path, obj, options) { `${path}: new Schema(...)`); } } - return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast); + return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast, this); } if (typeof cast !== 'undefined') { if (Array.isArray(cast) || cast.type === Array || cast.type == 'Array') { if (cast && cast.type == 'Array') { cast.type = Array; } - return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj); + return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj, null, this); } } @@ -1660,10 +1660,10 @@ Schema.prototype.interpretAsType = function(path, obj, options) { const childSchema = new Schema(castFromTypeKey, childSchemaOptions); childSchema.$implicitlyCreated = true; - return new MongooseTypes.DocumentArray(path, childSchema, obj); + return new MongooseTypes.DocumentArray(path, childSchema, obj, null, this); } else { // Special case: empty object becomes mixed - return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj); + return new MongooseTypes.Array(path, MongooseTypes.Mixed, obj, null, this); } } @@ -1672,7 +1672,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) { ? cast[options.typeKey] : cast; if (Array.isArray(type)) { - return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj); + return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj, null, this); } name = typeof type === 'string' @@ -1696,11 +1696,11 @@ Schema.prototype.interpretAsType = function(path, obj, options) { } } - return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options); + return new MongooseTypes.Array(path, cast || MongooseTypes.Mixed, obj, options, this); } if (type && type.instanceOfSchema) { - return new MongooseTypes.Subdocument(type, path, obj); + return new MongooseTypes.Subdocument(type, path, obj, this); } if (Buffer.isBuffer(type)) { diff --git a/lib/schema/array.js b/lib/schema/array.js index 8d8ff8d10a5..0849fbeb7e5 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -38,11 +38,12 @@ const emptyOpts = Object.freeze({}); * @param {SchemaType} cast * @param {Object} options * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaArray(key, cast, options, schemaOptions) { +function SchemaArray(key, cast, options, schemaOptions, parentSchema) { // lazy load EmbeddedDoc || (EmbeddedDoc = require('../types').Embedded); @@ -92,7 +93,7 @@ function SchemaArray(key, cast, options, schemaOptions) { !caster.$isArraySubdocument && !caster.$isSchemaMap) { const path = this.caster instanceof EmbeddedDoc ? null : key; - this.caster = new caster(path, castOptions); + this.caster = new caster(path, castOptions, schemaOptions, parentSchema); } else { this.caster = caster; if (!(this.caster instanceof EmbeddedDoc)) { @@ -105,7 +106,7 @@ function SchemaArray(key, cast, options, schemaOptions) { this.$isMongooseArray = true; - SchemaType.call(this, key, options, 'Array'); + SchemaType.call(this, key, options, 'Array', parentSchema); let defaultArr; let fn; @@ -494,7 +495,7 @@ SchemaArray.prototype.discriminator = function(...args) { SchemaArray.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, this.caster, options, this.schemaOptions); + const schematype = new this.constructor(this.path, this.caster, options, this.schemaOptions, this.parentSchema); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js index 955819eeeaa..7398d85e51b 100644 --- a/lib/schema/bigint.js +++ b/lib/schema/bigint.js @@ -14,12 +14,14 @@ const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeD * * @param {String} path * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaBigInt(path, options) { - SchemaType.call(this, path, options, 'BigInt'); +function SchemaBigInt(path, options, _schemaOptions, parentSchema) { + SchemaType.call(this, path, options, 'BigInt', parentSchema); } /** diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js index 6e48930b7ea..671ee040087 100644 --- a/lib/schema/boolean.js +++ b/lib/schema/boolean.js @@ -14,12 +14,14 @@ const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeD * * @param {String} path * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaBoolean(path, options) { - SchemaType.call(this, path, options, 'Boolean'); +function SchemaBoolean(path, options, _schemaOptions, parentSchema) { + SchemaType.call(this, path, options, 'Boolean', parentSchema); } /** diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js index 83563ed5c13..b1f9e887c5e 100644 --- a/lib/schema/buffer.js +++ b/lib/schema/buffer.js @@ -19,12 +19,14 @@ const CastError = SchemaType.CastError; * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaBuffer(key, options) { - SchemaType.call(this, key, options, 'Buffer'); +function SchemaBuffer(key, options, _schemaOptions, parentSchema) { + SchemaType.call(this, key, options, 'Buffer', parentSchema); } /** diff --git a/lib/schema/date.js b/lib/schema/date.js index cd14a447de4..a2a9cc84c74 100644 --- a/lib/schema/date.js +++ b/lib/schema/date.js @@ -19,12 +19,14 @@ const CastError = SchemaType.CastError; * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaDate(key, options) { - SchemaType.call(this, key, options, 'Date'); +function SchemaDate(key, options, _schemaOptions, parentSchema) { + SchemaType.call(this, key, options, 'Date', parentSchema); } /** diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js index 601dfe38413..854081cb573 100644 --- a/lib/schema/decimal128.js +++ b/lib/schema/decimal128.js @@ -15,12 +15,14 @@ const isBsonType = require('../helpers/isBsonType'); * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaDecimal128(key, options) { - SchemaType.call(this, key, options, 'Decimal128'); +function SchemaDecimal128(key, options, _schemaOptions, parentSchema) { + SchemaType.call(this, key, options, 'Decimal128', parentSchema); } /** diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 1a13274fb53..94b3ffd41c0 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -35,11 +35,12 @@ let Subdocument; * @param {Schema} schema * @param {Object} options * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaArray * @api public */ -function SchemaDocumentArray(key, schema, options, schemaOptions) { +function SchemaDocumentArray(key, schema, options, schemaOptions, parentSchema) { if (schema.options && schema.options.timeseries) { throw new InvalidSchemaOptionError(key, 'timeseries'); } @@ -59,7 +60,7 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { const EmbeddedDocument = _createConstructor(schema, options); EmbeddedDocument.prototype.$basePath = key; - SchemaArray.call(this, key, EmbeddedDocument, options); + SchemaArray.call(this, key, EmbeddedDocument, options, null, parentSchema); this.schema = schema; // EmbeddedDocument schematype options @@ -83,10 +84,15 @@ function SchemaDocumentArray(key, schema, options, schemaOptions) { } const $parentSchemaType = this; - this.$embeddedSchemaType = new DocumentArrayElement(key + '.$', { - ...(schemaOptions || {}), - $parentSchemaType - }); + this.$embeddedSchemaType = new DocumentArrayElement( + key + '.$', + { + ...(schemaOptions || {}), + $parentSchemaType + }, + schemaOptions, + parentSchema + ); this.$embeddedSchemaType.caster = this.Constructor; this.$embeddedSchemaType.schema = this.schema; @@ -529,7 +535,13 @@ SchemaDocumentArray.prototype.cast = function(value, doc, init, prev, options) { SchemaDocumentArray.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, this.schema, options, this.schemaOptions); + const schematype = new this.constructor( + this.path, + this.schema, + options, + this.schemaOptions, + this.parentSchema + ); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { schematype.requiredValidator = this.requiredValidator; diff --git a/lib/schema/documentArrayElement.js b/lib/schema/documentArrayElement.js index 5250b74b505..3d9b421ff2e 100644 --- a/lib/schema/documentArrayElement.js +++ b/lib/schema/documentArrayElement.js @@ -14,18 +14,20 @@ const getConstructor = require('../helpers/discriminator/getConstructor'); * * @param {String} path * @param {Object} options + * @param {Object} _schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaDocumentArrayElement(path, options) { +function SchemaDocumentArrayElement(path, options, _schemaOptions, parentSchema) { this.$parentSchemaType = options && options.$parentSchemaType; if (!this.$parentSchemaType) { throw new MongooseError('Cannot create DocumentArrayElement schematype without a parent'); } delete options.$parentSchemaType; - SchemaType.call(this, path, options, 'DocumentArrayElement'); + SchemaType.call(this, path, options, 'DocumentArrayElement', parentSchema); this.$isMongooseDocumentArrayElement = true; } diff --git a/lib/schema/double.js b/lib/schema/double.js index 327db7d5fe0..48296b0faaf 100644 --- a/lib/schema/double.js +++ b/lib/schema/double.js @@ -14,12 +14,14 @@ const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeD * * @param {String} path * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaDouble(path, options) { - SchemaType.call(this, path, options, 'Double'); +function SchemaDouble(path, options, _schemaOptions, parentSchema) { + SchemaType.call(this, path, options, 'Double', parentSchema); } /** diff --git a/lib/schema/int32.js b/lib/schema/int32.js index 018c481e90d..31c9d781351 100644 --- a/lib/schema/int32.js +++ b/lib/schema/int32.js @@ -15,12 +15,14 @@ const handleBitwiseOperator = require('./operators/bitwise'); * * @param {String} path * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaInt32(path, options) { - SchemaType.call(this, path, options, 'Int32'); +function SchemaInt32(path, options, _schemaOptions, parentSchema) { + SchemaType.call(this, path, options, 'Int32', parentSchema); } /** diff --git a/lib/schema/map.js b/lib/schema/map.js index cd1de644107..069b4657253 100644 --- a/lib/schema/map.js +++ b/lib/schema/map.js @@ -14,8 +14,8 @@ const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeD */ class SchemaMap extends SchemaType { - constructor(key, options) { - super(key, options, 'Map'); + constructor(key, options, _schemaOptions, parentSchema) { + super(key, options, 'Map', parentSchema); this.$isSchemaMap = true; } diff --git a/lib/schema/mixed.js b/lib/schema/mixed.js index bdb5f8d405b..93740640891 100644 --- a/lib/schema/mixed.js +++ b/lib/schema/mixed.js @@ -14,11 +14,13 @@ const utils = require('../utils'); * * @param {String} path * @param {Object} options + * @param {Object} _schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaMixed(path, options) { +function SchemaMixed(path, options, _schemaOptions, parentSchema) { if (options && options.default) { const def = options.default; if (Array.isArray(def) && def.length === 0) { @@ -32,7 +34,7 @@ function SchemaMixed(path, options) { } } - SchemaType.call(this, path, options, 'Mixed'); + SchemaType.call(this, path, options, 'Mixed', parentSchema); this[symbols.schemaMixedSymbol] = true; } diff --git a/lib/schema/number.js b/lib/schema/number.js index e7e33800c8a..37cdc246df6 100644 --- a/lib/schema/number.js +++ b/lib/schema/number.js @@ -19,12 +19,14 @@ const CastError = SchemaType.CastError; * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaNumber(key, options) { - SchemaType.call(this, key, options, 'Number'); +function SchemaNumber(key, options, _schemaOptions, parentSchema) { + SchemaType.call(this, key, options, 'Number', parentSchema); } /** diff --git a/lib/schema/objectId.js b/lib/schema/objectId.js index 89bd94d5915..e2a26a199ec 100644 --- a/lib/schema/objectId.js +++ b/lib/schema/objectId.js @@ -21,11 +21,13 @@ let Document; * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaObjectId(key, options) { +function SchemaObjectId(key, options, _schemaOptions, parentSchema) { const isKeyHexStr = typeof key === 'string' && key.length === 24 && /^[a-f0-9]+$/i.test(key); const suppressWarning = options && options.suppressWarning; if ((isKeyHexStr || typeof key === 'undefined') && !suppressWarning) { @@ -34,7 +36,7 @@ function SchemaObjectId(key, options) { '`Mongoose.Schema.ObjectId`. Set the `suppressWarning` option if ' + 'you\'re trying to create a hex char path in your schema.'); } - SchemaType.call(this, key, options, 'ObjectId'); + SchemaType.call(this, key, options, 'ObjectId', parentSchema); } /** diff --git a/lib/schema/string.js b/lib/schema/string.js index 3fedb60fb77..7cb52b9db65 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -19,14 +19,16 @@ const CastError = SchemaType.CastError; * * @param {String} key * @param {Object} options + * @param {Object} schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaString(key, options) { +function SchemaString(key, options, _schemaOptions, parentSchema) { this.enumValues = []; this.regExp = null; - SchemaType.call(this, key, options, 'String'); + SchemaType.call(this, key, options, 'String', parentSchema); } /** diff --git a/lib/schema/subdocument.js b/lib/schema/subdocument.js index c9e47944f3e..6b0c9aee8a3 100644 --- a/lib/schema/subdocument.js +++ b/lib/schema/subdocument.js @@ -32,11 +32,12 @@ module.exports = SchemaSubdocument; * @param {Schema} schema * @param {String} path * @param {Object} options + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaSubdocument(schema, path, options) { +function SchemaSubdocument(schema, path, options, parentSchema) { if (schema.options.timeseries) { throw new InvalidSchemaOptionError(path, 'timeseries'); } @@ -55,7 +56,7 @@ function SchemaSubdocument(schema, path, options) { this.schema = schema; this.$isSingleNested = true; this.base = schema.base; - SchemaType.call(this, path, options, 'Embedded'); + SchemaType.call(this, path, options, 'Embedded', parentSchema); } /*! @@ -415,7 +416,8 @@ SchemaSubdocument.prototype.clone = function() { const schematype = new this.constructor( this.schema, this.path, - { ...this.options, _skipApplyDiscriminators: true } + { ...this.options, _skipApplyDiscriminators: true }, + this.parentSchema ); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) { diff --git a/lib/schema/union.js b/lib/schema/union.js index e9f8e5b573a..e3a1b1ec5e2 100644 --- a/lib/schema/union.js +++ b/lib/schema/union.js @@ -14,8 +14,16 @@ const firstValueSymbol = Symbol('firstValue'); */ class Union extends SchemaType { + /** + * Create a Union schema type. + * + * @param {String} key the path in the schema for this schema type + * @param {Object} options SchemaType-specific options (must have 'of' as array) + * @param {Object} schemaOptions additional options from the schema this schematype belongs to + * @param {Schema} parentSchema the schema this schematype belongs to + */ constructor(key, options, schemaOptions, parentSchema) { - super(key, options, 'Union'); + super(key, options, 'Union', parentSchema); if (!options || !Array.isArray(options.of) || options.of.length === 0) { throw new Error('Union schema type requires an array of types'); } diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js index 727b51390e8..716cd24fc88 100644 --- a/lib/schema/uuid.js +++ b/lib/schema/uuid.js @@ -37,12 +37,14 @@ function binaryToString(uuidBin) { * * @param {String} key * @param {Object} options + * @param {Object} _schemaOptions + * @param {Schema} parentSchema * @inherits SchemaType * @api public */ -function SchemaUUID(key, options) { - SchemaType.call(this, key, options, 'UUID'); +function SchemaUUID(key, options, _schemaOptions, parentSchema) { + SchemaType.call(this, key, options, 'UUID', parentSchema); this.getters.push(function(value) { // For populated if (value != null && value.$__ != null) { diff --git a/lib/schemaType.js b/lib/schemaType.js index b2c27fed15c..3c062f07a36 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -37,10 +37,11 @@ const setOptionsForDefaults = { _skipMarkModified: true }; * @param {String} path * @param {SchemaTypeOptions} [options] See [SchemaTypeOptions docs](https://mongoosejs.com/docs/api/schematypeoptions.html) * @param {String} [instance] + * @param {Schema} [parentSchema] * @api public */ -function SchemaType(path, options, instance) { +function SchemaType(path, options, instance, parentSchema) { this[schemaTypeSymbol] = true; this.path = path; this.instance = instance; @@ -73,6 +74,10 @@ function SchemaType(path, options, instance) { const Options = this.OptionsConstructor || SchemaTypeOptions; this.options = new Options(options); + this.parentSchema = parentSchema; + // if (!parentSchema?.instanceOfSchema) { + // throw new Error('parentSchema must be an instance of Schema'); + // } this._index = null; if (utils.hasUserDefinedProperty(this.options, 'immutable')) { @@ -157,6 +162,30 @@ SchemaType.prototype.OptionsConstructor = SchemaTypeOptions; SchemaType.prototype.path; +/** + * Returns a plain JavaScript object representing this SchemaType. + * + * Typically used by `JSON.stringify()` or when calling `.toJSON()` on a SchemaType instance. + * Omits certain internal properties such as `parentSchema` that can cause circular references. + * + * #### Example: + * + * const schema = new Schema({ name: String }); + * const schematype = schema.path('name'); + * console.log(schematype.toJSON()); + * + * @function toJSON + * @memberOf SchemaType + * @instance + * @api public + */ + +SchemaType.prototype.toJSON = function toJSON() { + const res = { ...this }; + delete res.parentSchema; + return res; +}; + /** * The validators that Mongoose should run to validate properties at this SchemaType's path. * @@ -1751,7 +1780,7 @@ SchemaType.prototype.checkRequired = function(val) { SchemaType.prototype.clone = function() { const options = Object.assign({}, this.options); - const schematype = new this.constructor(this.path, options, this.instance); + const schematype = new this.constructor(this.path, options, this.instance, this.parentSchema); schematype.validators = this.validators.slice(); if (this.requiredValidator !== undefined) schematype.requiredValidator = this.requiredValidator; if (this.defaultValue !== undefined) schematype.defaultValue = this.defaultValue; From 1586fc6e87ea9894801859cc6bb70e33a0056d0e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Nov 2025 16:45:50 -0500 Subject: [PATCH 3/3] refactor: move map embedded schematype instantiation to schema/map and out of schema, clean up some more inconsistencies --- lib/cast.js | 2 +- lib/helpers/query/castUpdate.js | 2 +- lib/schema.js | 39 ------------------------------- lib/schema/array.js | 6 ++++- lib/schema/map.js | 41 +++++++++++++++++++++++++++++++++ lib/schemaType.js | 3 --- lib/types/map.js | 3 ++- 7 files changed, 50 insertions(+), 46 deletions(-) diff --git a/lib/cast.js b/lib/cast.js index 03cbb3415c2..6f75d9bfe37 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -210,7 +210,7 @@ module.exports = function cast(schema, obj, options, context) { } if (geo) { - const numbertype = new Types.Number('__QueryCasting__'); + const numbertype = new Types.Number('__QueryCasting__', null, null, schema); let value = val[geo]; if (val.$maxDistance != null) { diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index b4a8a6b9d92..081bc98c390 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -428,7 +428,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) { if (obj[key] == null) { throw new CastError('String', obj[key], `${prefix}${key}.$rename`); } - const schematype = new SchemaString(`${prefix}${key}.$rename`); + const schematype = new SchemaString(`${prefix}${key}.$rename`, null, null, schema); obj[key] = schematype.castForQuery(null, obj[key], context); continue; } diff --git a/lib/schema.js b/lib/schema.js index c0f6e8fec7d..01a9e12a76b 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1741,48 +1741,9 @@ Schema.prototype.interpretAsType = function(path, obj, options) { const schemaType = new MongooseTypes[name](path, obj, options, this); - if (schemaType.$isSchemaMap) { - createMapNestedSchemaType(this, schemaType, path, obj, options); - } - return schemaType; }; -/*! - * ignore - */ - -function createMapNestedSchemaType(schema, schemaType, path, obj, options) { - const mapPath = path + '.$*'; - let _mapType = { type: {} }; - if (utils.hasUserDefinedProperty(obj, 'of')) { - const isInlineSchema = utils.isPOJO(obj.of) && - Object.keys(obj.of).length > 0 && - !utils.hasUserDefinedProperty(obj.of, schema.options.typeKey); - if (isInlineSchema) { - _mapType = { [schema.options.typeKey]: new Schema(obj.of) }; - } else if (utils.isPOJO(obj.of)) { - _mapType = Object.assign({}, obj.of); - } else { - _mapType = { [schema.options.typeKey]: obj.of }; - } - - if (_mapType[schema.options.typeKey] && _mapType[schema.options.typeKey].instanceOfSchema) { - const subdocumentSchema = _mapType[schema.options.typeKey]; - subdocumentSchema.eachPath((subpath, type) => { - if (type.options.select === true || type.options.select === false) { - throw new MongooseError('Cannot use schema-level projections (`select: true` or `select: false`) within maps at path "' + path + '.' + subpath + '"'); - } - }); - } - - if (utils.hasUserDefinedProperty(obj, 'ref')) { - _mapType.ref = obj.ref; - } - } - schemaType.$__schemaType = schema.interpretAsType(mapPath, _mapType, options); -} - /** * Iterates the schemas paths similar to Array#forEach. * diff --git a/lib/schema/array.js b/lib/schema/array.js index 0849fbeb7e5..2edf2f20dc7 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -93,7 +93,11 @@ function SchemaArray(key, cast, options, schemaOptions, parentSchema) { !caster.$isArraySubdocument && !caster.$isSchemaMap) { const path = this.caster instanceof EmbeddedDoc ? null : key; - this.caster = new caster(path, castOptions, schemaOptions, parentSchema); + if (caster === SchemaArray) { + this.caster = new caster(path, castOptions, schemaOptions, null, parentSchema); + } else { + this.caster = new caster(path, castOptions, schemaOptions, parentSchema); + } } else { this.caster = caster; if (!(this.caster instanceof EmbeddedDoc)) { diff --git a/lib/schema/map.js b/lib/schema/map.js index 069b4657253..44d0ae9b0ba 100644 --- a/lib/schema/map.js +++ b/lib/schema/map.js @@ -8,6 +8,9 @@ const MongooseMap = require('../types/map'); const SchemaMapOptions = require('../options/schemaMapOptions'); const SchemaType = require('../schemaType'); const createJSONSchemaTypeDefinition = require('../helpers/createJSONSchemaTypeDefinition'); +const MongooseError = require('../error/mongooseError'); +const Schema = require('../schema'); +const utils = require('../utils'); /*! * ignore @@ -17,6 +20,9 @@ class SchemaMap extends SchemaType { constructor(key, options, _schemaOptions, parentSchema) { super(key, options, 'Map', parentSchema); this.$isSchemaMap = true; + + // Create the nested schema type for the map values + this._createNestedSchemaType(parentSchema, key, options, _schemaOptions); } set(option, value) { @@ -117,4 +123,39 @@ SchemaMap.prototype.OptionsConstructor = SchemaMapOptions; SchemaMap.defaultOptions = {}; +/*! + * ignore + */ + +SchemaMap.prototype._createNestedSchemaType = function _createNestedSchemaType(schema, path, obj, options) { + const mapPath = path + '.$*'; + let _mapType = { type: {} }; + if (utils.hasUserDefinedProperty(obj, 'of')) { + const isInlineSchema = utils.isPOJO(obj.of) && + Object.keys(obj.of).length > 0 && + !utils.hasUserDefinedProperty(obj.of, schema.options.typeKey); + if (isInlineSchema) { + _mapType = { [schema.options.typeKey]: new Schema(obj.of) }; + } else if (utils.isPOJO(obj.of)) { + _mapType = Object.assign({}, obj.of); + } else { + _mapType = { [schema.options.typeKey]: obj.of }; + } + + if (_mapType[schema.options.typeKey] && _mapType[schema.options.typeKey].instanceOfSchema) { + const subdocumentSchema = _mapType[schema.options.typeKey]; + subdocumentSchema.eachPath((subpath, type) => { + if (type.options.select === true || type.options.select === false) { + throw new MongooseError('Cannot use schema-level projections (`select: true` or `select: false`) within maps at path "' + path + '.' + subpath + '"'); + } + }); + } + + if (utils.hasUserDefinedProperty(obj, 'ref')) { + _mapType.ref = obj.ref; + } + } + this.$__schemaType = schema.interpretAsType(mapPath, _mapType, options); +}; + module.exports = SchemaMap; diff --git a/lib/schemaType.js b/lib/schemaType.js index 3c062f07a36..f81e0816225 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -75,9 +75,6 @@ function SchemaType(path, options, instance, parentSchema) { const Options = this.OptionsConstructor || SchemaTypeOptions; this.options = new Options(options); this.parentSchema = parentSchema; - // if (!parentSchema?.instanceOfSchema) { - // throw new Error('parentSchema must be an instance of Schema'); - // } this._index = null; if (utils.hasUserDefinedProperty(this.options, 'immutable')) { diff --git a/lib/types/map.js b/lib/types/map.js index 6703a932883..c5eb8da0d45 100644 --- a/lib/types/map.js +++ b/lib/types/map.js @@ -2,6 +2,7 @@ const Mixed = require('../schema/mixed'); const MongooseError = require('../error/mongooseError'); +const assert = require('assert'); const clone = require('../helpers/clone'); const deepEqual = require('../utils').deepEqual; const getConstructorName = require('../helpers/getConstructorName'); @@ -41,7 +42,7 @@ class MongooseMap extends Map { this.$__pathRelativeToParent = null; } - this.$__schemaType = schemaType == null ? new Mixed(path) : schemaType; + this.$__schemaType = schemaType; this.$__runDeferred(); }