Skip to content

Conversation

@vkarpov15
Copy link
Collaborator

Summary

Implementing your own container type is a pain unless the container type happens to extend from Array, because otherwise you need to handle interpreting of by yourself. For example, imagine { type: Set, of: { type: String, required: true } } - right now anyone implementing a set schematype would have to re-implement interpretAsType semantics themselves. We had to do a bit of a hack to avoid having to re-implement interpretAsType for unions, but with this fix we can remove the hack as well.

Examples

…sType to make implementing custom container types easier
@vkarpov15 vkarpov15 added this to the 8.20 milestone Oct 27, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This refactor improves the extensibility of custom container types in Mongoose by passing the parent schema as a parameter to SchemaType constructors. Previously, implementing custom container types that needed to interpret nested schemas (like { type: Set, of: { type: String } }) required re-implementing interpretAsType logic or using workarounds like the options.parentSchema hack used in the Union type.

Key changes:

  • Added parentSchema parameter to all SchemaType constructors via interpretAsType
  • Removed the Union-specific hack that attached parent schema to options
  • Union type now receives parent schema directly as a constructor parameter

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/schema.js Updated interpretAsType to pass this (parent schema) as fourth parameter to SchemaType constructors, removing Union-specific workaround
lib/schema/union.js Modified Union constructor to accept parentSchema parameter instead of accessing it from options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@hasezoey hasezoey left a comment

Choose a reason for hiding this comment

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

LGTM, though it seems Github CI's Node 22 does not like some change here, though any other version just works.

The CI errors in question

Runner: Node 22 MongoDB 6.0.15 OS ubuntu-22.04
Full Node Version in runner: 22.21.0

  1) Double
       cast errors
         when a non-numeric string is provided to an Double field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Double failed for value "helloworld" (type string) at path "myDouble" because of "SyntaxError"' == 'Cast to Double failed for value "helloworld" (type string) at path "myDouble"'
      + expected - actual
      -Cast to Double failed for value "helloworld" (type string) at path "myDouble" because of "SyntaxError"
      +Cast to Double failed for value "helloworld" (type string) at path "myDouble"
      
      at Context.<anonymous> (test/double.test.js:284:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  2) Int32
       cast errors
         when a non-integer decimal input is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "-42.4" (type number) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "-42.4" (type number) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "-42.4" (type number) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "-42.4" (type number) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:304:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  3) Int32
       cast errors
         when a non-numeric string is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "helloworld" (type string) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "helloworld" (type string) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "helloworld" (type string) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "helloworld" (type string) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:322:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  4) Int32
       cast errors
         when a non-integer decimal string is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "1.2" (type string) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "1.2" (type string) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "1.2" (type string) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "1.2" (type string) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:340:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  5) Int32
       cast errors
         when NaN is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "NaN" (type number) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "NaN" (type number) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "NaN" (type number) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "NaN" (type number) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:358:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  6) Int32
       cast errors
         when value above INT32_MAX is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "2147483648" (type number) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "2147483648" (type number) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "2147483648" (type number) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "2147483648" (type number) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:376:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  7) Int32
       cast errors
         when value below INT32_MIN is provided to an Int32 field
           throws a CastError upon validation:
      AssertionError [ERR_ASSERTION]: 'Cast to Int32 failed for value "-2147483649" (type number) at path "myInt" because of "SyntaxError"' == 'Cast to Int32 failed for value "-2147483649" (type number) at path "myInt"'
      + expected - actual
      -Cast to Int32 failed for value "-2147483649" (type number) at path "myInt" because of "SyntaxError"
      +Cast to Int32 failed for value "-2147483649" (type number) at path "myInt"
      
      at Context.<anonymous> (test/int32.test.js:394:16)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  8) model: findOneAndUpdate:
       bug fixes
         single nested doc cast errors (gh-3602):
      AssertionError [ERR_ASSERTION]: 'Cast to Number failed for value "not a num" (type string) at path "street" because of "SyntaxError"' == 'Cast to Number failed for value "not a num" (type string) at path "street"'
      + expected - actual
      -Cast to Number failed for value "not a num" (type string) at path "street" because of "SyntaxError"
      +Cast to Number failed for value "not a num" (type string) at path "street"
      
      at Context.<anonymous> (test/model.findOneAndUpdate.test.js:1357:14)
      at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

  9) validation docs
       Cast Errors:
      AssertionError [ERR_ASSERTION]: 'Cast to Number failed for value "not a number" (type string) at path "numWheels" because of "SyntaxError"' == 'Cast to Number failed for value "not a number" (type string) at path "numWheels"'
      + expected - actual
      -Cast to Number failed for value "not a number" (type string) at path "numWheels" because of "SyntaxError"
      +Cast to Number failed for value "not a number" (type string) at path "numWheels"
      
      at Context.<anonymous> (test/docs/validation.test.js:384:12)
      at process.processImmediate (node:internal/timers:485:21)

@vkarpov15 vkarpov15 changed the base branch from master to 8.20 November 1, 2025 20:41
@vkarpov15 vkarpov15 requested review from Copilot and hasezoey November 3, 2025 21:46
@vkarpov15
Copy link
Collaborator Author

@hasezoey can you please take another look? I did a bunch more refactor to make the constructor signatures more consistent - I was able to make it so that all schema types take in a parentSchema param, although there's still some inconsistencies in function signatures that I would like to fix in the 9.0 branch.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

lib/types/map.js:5

  • Unused variable assert.
const assert = require('assert');

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


const Mixed = require('../schema/mixed');
const MongooseError = require('../error/mongooseError');
const assert = require('assert');
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

The assert module is imported but never used in this file. This import should be removed to keep dependencies clean.

Suggested change
const assert = require('assert');

Copilot uses AI. Check for mistakes.
this.$isSchemaMap = true;

// Create the nested schema type for the map values
this._createNestedSchemaType(parentSchema, key, options, _schemaOptions);
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

If parentSchema is null or undefined, calling _createNestedSchemaType will result in a runtime error when it tries to access schema.options.typeKey (line 136) and schema.interpretAsType (line 158). Add a null check or ensure parentSchema is always provided.

Suggested change
this._createNestedSchemaType(parentSchema, key, options, _schemaOptions);
if (parentSchema != null) {
this._createNestedSchemaType(parentSchema, key, options, _schemaOptions);
}

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

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

If parentSchema is null or undefined, this will throw a runtime error. Consider adding validation to ensure parentSchema is provided, or add a guard clause in the constructor.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@hasezoey hasezoey left a comment

Choose a reason for hiding this comment

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

LGTM, aside from lint and a few nitpicks that are not directly related to this PR.

*
* @param {String} path
* @param {Object} options
* @param {Object} _schemaOptions
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @param {Object} _schemaOptions
* @param {Object} schemaOptions

The other schematypes dont refer to it with underscore in the jsdoc when unused (re SchemaBoolean, SchemaBuffer, etc)

constructor(key, options, _schemaOptions, parentSchema) {
super(key, options, 'Map', parentSchema);
this.$isSchemaMap = true;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change

Comment on lines 15 to 17
/*!
* ignore
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

SchemaMap is public right? This should likely also have a proper jsdoc then like the other schema types (though not directly related to this PR)

class SchemaMap extends SchemaType {
constructor(key, options) {
super(key, options, 'Map');
constructor(key, options, _schemaOptions, parentSchema) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
constructor(key, options, _schemaOptions, parentSchema) {
constructor(key, options, schemaOptions, parentSchema) {

Its not unused

this.$isSchemaMap = true;

// Create the nested schema type for the map values
this._createNestedSchemaType(parentSchema, key, options, _schemaOptions);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
this._createNestedSchemaType(parentSchema, key, options, _schemaOptions);
this._createNestedSchemaType(parentSchema, key, options, schemaOptions);

Re: its not unused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants