news 2026/9/10 21:57:08

Mongoose 自定义类型转换(Custom Casting)完全指南:用 SchemaType.cast() 覆盖内置 Casting

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mongoose 自定义类型转换(Custom Casting)完全指南:用 SchemaType.cast() 覆盖内置 Casting

Mongoose 自定义类型转换(Custom Casting)完全指南:用 SchemaType.cast() 覆盖内置 Casting

【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose

本指南讲解 Mongoose 的**自定义类型转换(Custom Casting)**机制——通过SchemaType.cast()全局覆盖某个 SchemaType 的内置转换函数,从而让类型转换规则完全贴合业务需求。文中以“把日文数字字符串'二'转成数字2”为主线案例,并深入 lib/schemaType.js、lib/schema/number.js、lib/cast/number.js 等源码,剖析自定义转换的底层调用链与错误包装机制。读完你将掌握:Mongoose 内置转换何时会失败、如何无侵入地覆盖默认转换、如何委托原函数、如何彻底禁用转换,以及如何在全局与单个 SchemaType 实例两个层面定制转换行为。

什么是 Casting,为什么需要自定义

在 Mongoose 中,**Casting(类型转换)**指把传入的任意值(如 HTTP 请求里的字符串、表单提交的文本)转换成 Schema 中声明的目标类型的过程。例如 Schema 声明age: Number时,Mongoose 会在赋值、校验、查询时尝试把值转换为数字。

Mongoose 5.4.0 起引入了若干种全局配置 SchemaType的能力,其中就包括SchemaType.cast()这个函数,它允许开发者覆盖 Mongoose 内置的转换逻辑。

内置转换是“尽力而为”的:它覆盖了字符串数字、布尔值、包装对象等常见情况,但无法覆盖所有业务场景。例如,默认情况下 Mongoose 无法把包含日文数字的字符串'二'转换为数字,会直接抛出转换失败错误(CastError)。

默认行为:日文数字触发 CastError

先看默认行为。声明age: Number后,给文档赋值为字符串'二'并执行同步校验:

const schema = new mongoose.Schema({ age: Number }); const Model = mongoose.model('Test', schema); const doc = new Model({ age: '二' }); const err = doc.validateSync(); // "Cast to Number failed for value "二" (type string) at path "age"" err.message;

validateSync()返回的err.message会包含类似Cast to Number failed for value "二" (type string) at path "age"的信息。也就是说,字符串'二'无法被内置的数字转换函数接受,校验失败。

这条行为同样被仓库测试锁定:test/docs/custom-casting.test.js 中的casting error用例断言了错误信息中必须包含Cast to Number failed for value "二" (type string) at path "age"

全局覆盖:mongoose.Number.cast(fn)

SchemaType的子类(如Number)上都挂载了静态的cast()方法,它同时承担“读取”与“写入”两个职责:

  • 无参数调用mongoose.Number.cast()返回当前生效的转换函数(即原来的内置转换)。
  • 传入函数调用mongoose.Number.cast(fn)设置新的转换函数,此后所有 Number 路径的转换都会走fn

于是可以这样为数字类型扩展“日文数字”支持:

// 先保存当前内置的转换函数 const originalCast = mongoose.Number.cast(); // 用自定义函数覆盖,遇到 '二' 返回 2,其余委托给内置转换 mongoose.Number.cast(v => { if (v === '二') { return 2; } return originalCast(v); }); const schema = new mongoose.Schema({ age: Number }); const Model = mongoose.model('Test', schema); const doc = new Model({ age: '二' }); const err = doc.validateSync(); err; // null —— 校验通过 doc.age; // 2 —— 已被转换为数字

关键点有两个:

  1. 先取出原函数再委托const originalCast = mongoose.Number.cast()拿到内置转换;自定义函数对无法识别的值调用originalCast(v)走原有逻辑,避免破坏默认行为。
  2. 覆盖是全局的:该设置在mongoose实例(或mongoose单例)层面生效,之后新建的所有 Schema 中的 Number 路径都会使用新转换。

该用例在 test/docs/custom-casting.test.js 中由casting override测试验证,断言errnulldoc.age === 2

彻底禁用转换:cast(false) 与严格模式

除了传入自定义函数,cast()还支持传入false完全关闭该类型的转换。在 lib/schema/number.js 中:

SchemaNumber.cast = function cast(caster) { if (arguments.length === 0) { return this._cast; } if (caster === false) { caster = this._defaultCaster; } this._cast = caster; return this._cast; };

传入false时,会回退到_defaultCaster——一个只接受number类型、其余一律抛错的严格函数:

SchemaNumber._defaultCaster = v => { if (typeof v !== 'number') { throw new Error(); } return v; };

也就是说:

// 禁用 Number 的一切隐式转换 mongoose.Number.cast(false); // 之后即使传 '123' 这样的纯数字字符串也会抛错,只有真正的 number 才能通过

这一点在 lib/schemaType.js 的基类实现里略有不同:基类的false回退为v => v(恒等函数,原样返回);而 Number 的false回退为只接受number的严格校验器。因此“禁用转换”的语义因类型而异,使用前应结合目标类型的实现确认。

官方文档注释也给出了这一用法(见 lib/schema/number.js):mongoose.Number.cast(false)等价于“完全禁用转换”;与之对应,还可以用mongoose.Number.cast(v => { if (v === '') { return 0; } return original(v); })让空字符串转换为0

实例级定制:castFunction()

上面的cast()类(构造器)级别的,影响该类型的所有路径。若只想影响某一个 SchemaType 实例(某一条具体路径),可以使用SchemaType.prototype.castFunction()(lib/schemaType.js):

const number = new mongoose.Number('mypath', {}); number.castFunction(v => { // 只影响 'mypath' 这条路径:只允许 number 或 undefined assert.ok(v === undefined || typeof v === 'number'); return v; });

castFunction(caster, message)同样支持无参读取、传false回退到默认、传字符串设置自定义错误消息。在 lib/schema/number.js 的实例cast()方法中可以看到两者的优先级:优先使用实例级this._castFunction,其次回退到构造器级this.constructor.cast()返回的全局转换函数。这意味着你可以先用mongoose.Number.cast()做全局兜底,再对个别路径用castFunction()做细粒度覆盖。

底层原理:转换失败如何变成 CastError

自定义转换的执行链路可以从 lib/schema/number.js 的实例cast()方法看清:

SchemaNumber.prototype.cast = function(value, doc, init, prev, options) { // 1. 处理引用(populate ref)与文档对象(取 value._id) if (typeof value !== 'number' && SchemaType._isRef(this, value, doc, init)) { if (value == null || utils.isNonBuiltinObject(value)) { return this._castRef(value, doc, init, options); } } const val = value?._id !== undefined ? value._id : value; // 2. 选择转换函数:实例级优先,其次构造器级 let castNumber; if (typeof this._castFunction === 'function') { castNumber = this._castFunction; } else if (typeof this.constructor.cast === 'function') { castNumber = this.constructor.cast(); } else { castNumber = SchemaNumber.cast(); } // 3. 执行转换,抛出的任何错误统一包装为 CastError try { return castNumber(val); } catch (err) { throw new CastError('Number', val, this.path, err, this); } };

由此可以看出:

  • 转换函数的选择顺序是实例级_castFunction→ 构造器级cast()→ 兜底SchemaNumber.cast()
  • 自定义函数中throw的任何错误,都会被捕获并重新包装为CastError('Number', ...),与 Mongoose 内置的报错风格保持一致;
  • _id字段,会先取出value._id再转换,兼容传入文档对象的情况。

内置的默认转换函数实现在 lib/cast/number.js,其完整规则为:

输入值转换结果
null/undefined原样返回(视为合法)
空字符串''返回null
字符串 / 布尔值Number(val)转换
NaN结果抛出Cast to Number failed: value is not a valid number
Number包装对象返回valueOf()
普通number直接返回
valueOf函数的对象Number(val.valueOf())
toString且能转成数字的对象返回Number(val)
其余情况抛出Cast to Number failed: value is not a valid number

这正是自定义转换“委托原函数”时的行为基准。

不止 Number:其他 SchemaType 同样支持

cast()SchemaType基类的能力,因此StringDateBooleanObjectIdDecimal128DoubleInt32等内置类型也都支持全局自定义转换。仓库测试给出了多处佐证:

  • test/schematype.cast.test.js(对应 issue gh-7045)覆盖了ObjectIdBoolean等类型的自定义转换:例如对ObjectId自定义转换,让字符串'special'映射到合法的 ObjectId,同时保持基类Schema.ObjectId的行为不变(通过子类继承实现隔离):
class CustomObjectId extends Schema.ObjectId {} CustomObjectId.cast(v => { if (v === 'special') { return original.objectid('0'.repeat(24)); } return original.objectid(v); });
  • 同一文件还验证了Schema.ObjectId.cast(false)后,'000000000000000000000000'这类字符串会抛出CastError,只有真正的ObjectId实例能通过(test/schematype.cast.test.js)。
  • test/double.test.js 与 test/int32.test.js 分别演示了Double.cast(fn)Int32.cast(fn)的覆盖写法。
  • test/schematype.test.js 展示了在 SchemaType 实例上直接调用schemaType.cast(...)的用法。

这些测试都遵循同一模式:先在beforeEach中保存原转换函数,在afterEach中恢复,避免测试间相互污染。

使用注意事项

  1. 全局覆盖影响所有 Schemamongoose.Number.cast(fn)一旦设置,后续创建的所有使用Number的路径都会受影响(已有 Schema 实例在创建时已捕获转换函数,行为视具体版本而定)。因此务必保存并委托原函数,并在不再需要时恢复,例如:
    const originalCast = mongoose.Number.cast(); mongoose.Number.cast(v => /* 自定义逻辑 */); // ...业务代码... mongoose.Number.cast(originalCast); // 恢复
  2. 错误会被包装成 CastError:自定义函数里throw new Error('...')会在外层被包装为带路径信息的CastError(见 lib/schema/number.js),调用方可通过err.name === 'CastError'判断。
  3. 禁用转换语义因类型而异cast(false)在基类回退为恒等函数,在Number回退为“仅接受 number”的严格函数,使用前应查看对应类型源码确认。
  4. 实例级覆盖更安全:如果只想影响单条路径,优先使用SchemaType.prototype.castFunction()(lib/schemaType.js),避免污染全局。
  5. 与校验、查询联动:自定义转换同时作用于文档赋值/校验和查询条件(查询走castForQuery,见 lib/schema/number.js),因此转换函数需要能够处理来自查询过滤器的值。

延伸阅读

  • 教程原文:docs/tutorials/custom-casting.md
  • 基类实现:lib/schemaType.js(静态cast、实例castFunction、原型cast
  • Number 实现:lib/schema/number.js(构造器级cast_defaultCaster)、lib/schema/number.js(实例cast与 CastError 包装)
  • 内置转换函数:lib/cast/number.js、lib/cast/string.js、lib/cast/boolean.js
  • 类型注册:lib/mongoose.js(mongoose.Number = SchemaTypes.Number
  • 测试用例:test/docs/custom-casting.test.js、test/schematype.cast.test.js

【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 21:55:04

搜狗输入法快捷短语设置与高效使用指南

1. 搜狗输入法快捷短语功能解析 作为国内主流输入法之一,搜狗输入法的快捷短语功能是提升输入效率的利器。这个功能允许用户将常用语句(如地址、联系方式、固定回复等)设置为特定缩写,通过输入缩写快速调出完整内容。实测在客服回…

作者头像 李华
网站建设 2026/9/10 21:54:53

无线传感器网络多跳传输安全与噪声优化策略

1. 无线传感器网络中的多跳传输挑战 在野外监测、工业物联网和军事侦察等场景中,无线传感器网络(WSNs)常常需要面对复杂的传输环境。当节点分布范围超过单跳通信距离时,数据必须通过多跳中继才能到达汇聚节点。这种多跳传输模式带…

作者头像 李华