Mongoose读取数据库中已存在的必填字段userId为undefined导致save()失败的问题求助
我完全能理解你现在的困惑——明明数据库里userId字段好好的,用Compass和原生查询都能看到,结果Mongoose读出来就成了undefined,调用save()还触发必填校验失败,甚至冒出个完全不相关的Cannot read properties of undefined (reading 'options')错误,这简直离谱!
结合你给出的版本(mongoose 6.5.5、mongodb驱动5.1.0)和描述的细节,我认为最核心的问题是Schema字段类型和数据库实际存储类型不匹配,下面给你拆解原因和解决方法:
最可能的根因:Schema类型与数据库存储类型不兼容
你提到数据库里的userId是ObjectId类型,但你的Schema里定义的是type: String:
userId: { type: String, required: true, ref: 'user' }
Mongoose在把数据库中的文档转换为Mongoose Document对象(也就是你用findById()/findOne()拿到的实例)时,会严格按照Schema的类型做校验和转换。当数据库里的ObjectId转成String的过程中,由于你用的Mongoose版本相对较旧,搭配MongoDB驱动5.1.0可能存在小版本适配bug,就会导致这个字段解析失败,最终变成undefined。
而那个莫名其妙的options读取错误,大概率是Mongoose内部在处理类型不匹配的字段时抛出的未被正确格式化的异常——等你修复了类型问题,这个错误会自动消失。
解决方法
1. 修正Schema类型(推荐,从根源解决)
把Schema中userId的类型改成和数据库一致的ObjectId:
const mongoose = require('mongoose'); const eventSchema = new mongoose.Schema({ userId: { type: mongoose.Types.ObjectId, // 改为ObjectId类型,匹配数据库存储 required: true, ref: 'user' }, // 其他字段... });
如果你的业务代码里需要把userId当成字符串使用,可以加个transform选项,在把Document转成普通对象时自动转成字符串:
userId: { type: mongoose.Types.ObjectId, required: true, ref: 'user', transform: (val) => val.toString() // 读取时自动转字符串 }
2. 临时Workaround(不推荐,仅应急)
如果暂时不想修改Schema,可以在fetch文档后,直接从_doc属性(Mongoose Document的原始数据库数据)中取出userId,手动赋值回去:
const event = await Model.findById(eventId); // 从原始数据中取userId,转成字符串后赋值 if (!event.userId) { event.userId = event._doc.userId.toString(); } // 再进行更新和save操作
这个方法本质上和你现在的“hack”类似,但更明确地取原始数据,避免Mongoose的类型解析干扰。
3. 版本兼容性排查
你使用的mongoose6.5.5是2022年8月的版本,mongodb驱动5.1.0是2022年10月的版本,两者之间可能存在小的适配问题。可以尝试:
- 把Mongoose升级到6.x系列的最新稳定版(比如6.12.0)
- 或者把MongoDB驱动降到4.x版本(比如4.17.0)
升级后再测试是否还会出现字段解析异常的问题。
额外排查点
虽然你已经排除了大部分情况,但可以再确认:
- 有没有给userId字段加自定义的getter函数?如果有,可能在某些场景下返回了
undefined - 有没有开启Mongoose的
strict模式?不过strict模式默认是开启的,只会过滤Schema中没有的字段,不会影响已存在的字段
验证方法
修改Schema类型后,重新用findById()获取文档,console.log(event.userId)应该能看到正确的ObjectId(或字符串,如果你加了transform),此时再调用save()就不会触发必填字段的校验错误了,那个options的异常也会消失。
内容来源于stack exchange

