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 —— 已被转换为数字关键点有两个:
- 先取出原函数再委托:
const originalCast = mongoose.Number.cast()拿到内置转换;自定义函数对无法识别的值调用originalCast(v)走原有逻辑,避免破坏默认行为。 - 覆盖是全局的:该设置在
mongoose实例(或mongoose单例)层面生效,之后新建的所有 Schema 中的 Number 路径都会使用新转换。
该用例在 test/docs/custom-casting.test.js 中由casting override测试验证,断言err为null且doc.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基类的能力,因此String、Date、Boolean、ObjectId、Decimal128、Double、Int32等内置类型也都支持全局自定义转换。仓库测试给出了多处佐证:
- test/schematype.cast.test.js(对应 issue gh-7045)覆盖了
ObjectId、Boolean等类型的自定义转换:例如对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中恢复,避免测试间相互污染。
使用注意事项
- 全局覆盖影响所有 Schema:
mongoose.Number.cast(fn)一旦设置,后续创建的所有使用Number的路径都会受影响(已有 Schema 实例在创建时已捕获转换函数,行为视具体版本而定)。因此务必保存并委托原函数,并在不再需要时恢复,例如:const originalCast = mongoose.Number.cast(); mongoose.Number.cast(v => /* 自定义逻辑 */); // ...业务代码... mongoose.Number.cast(originalCast); // 恢复 - 错误会被包装成 CastError:自定义函数里
throw new Error('...')会在外层被包装为带路径信息的CastError(见 lib/schema/number.js),调用方可通过err.name === 'CastError'判断。 - 禁用转换语义因类型而异:
cast(false)在基类回退为恒等函数,在Number回退为“仅接受 number”的严格函数,使用前应查看对应类型源码确认。 - 实例级覆盖更安全:如果只想影响单条路径,优先使用
SchemaType.prototype.castFunction()(lib/schemaType.js),避免污染全局。 - 与校验、查询联动:自定义转换同时作用于文档赋值/校验和查询条件(查询走
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),仅供参考