When the classToClass or plainToClass methods are used in conjunction with and ObjectId instance (using the latest version of the bson module) the object id becomes a completely different one
Minimal code-snippet showcasing the problem
npm install bson class-transformer
import { ObjectId } from 'bson';
import { plainToClass } from 'class-transformer';
import assert from 'assert';
class User {
_id!: ObjectId;
}
const plainUser = { _id: new ObjectId() };
const user = plainToClass(User, plainUser);
assert(user._id.toHexString() === plainUser._id.toHexString());
Assuming how this module works I expect user._id to be a different instance than plainUser._id but both sharing the same hexadecimal value
assert(user._id instanceof ObjectId);
assert(user._id !== plainUser._id);
assert(user._id.toHexString() === plainUser._id.toHexString());
The id is converted into a completely different one
Hey @leon19, your question made me curious so I ran your test and indeed got something like :

I investigated a bit and the reason is :
bson new ObjectId() is going to produce an instance with a different hex stringclass-transformer is going to handle the deserialization (it tends to create a new instance of the types it's going to meet while deserializing I think).Long story short, you could do something like this to overcome this problem :
import { ObjectId } from 'bson';
import { plainToClass, Transform } from 'class-transformer';
import assert from 'assert';
class User {
// notice how I do not use the first parameter,
// which would provide the new instance created by `class-transformer` (and wrong hex string)
// second parameter gives access to the original object being deserialized instead
@Transform((_, from: any) => from._id, { toClassOnly: true })
_id!: ObjectId;
}
const plainUser = { _id: new ObjectId() };
const user = plainToClass(User, plainUser);
assert(user._id.toHexString() === plainUser._id.toHexString());
I made a sample repo covering different cases with tests here, if you want to have deeper look.
Hope this helps 馃槂
To improve upon @Roms1383 solution, you should create a new decorator so that you can capture the property name rather than hardcoding it to _id.
export const ExposeId = (options?: ExposeOptions) =>
((target: Object, propertyKey: string) => {
Transform((_, obj) => obj[propertyKey])(target, propertyKey);
});
Usage:
@Schema()
class Cat {
@ExposeId()
_id: Types.ObjectId;
}
This is the expected behavior. @Roms1383 and @Rush answer is the correct way to work around this. I will leave this issue open until this is documented.
This behavior has changed in 90feca36d2b44b6c66428ccef9402b13b58f7e2e. Functions and constructors are not called anymore when doing plain-to-class transformation.
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
Hey @leon19, your question made me curious so I ran your test and indeed got something like :
I investigated a bit and the reason is :
bsonnew ObjectId()is going to produce an instance with a different hex stringclass-transformeris going to handle the deserialization (it tends to create a new instance of the types it's going to meet while deserializing I think).Long story short, you could do something like this to overcome this problem :
I made a sample repo covering different cases with tests here, if you want to have deeper look.
Hope this helps 馃槂