This is related to https://github.com/typestack/class-transformer/issues/282, where they realized they were missing the emitDecoratorMetadata compiler option...
According to the docs, it should be possible to work with an ES6 map like Map
When trying to use the plainToClass function, it does not correctly transform to a Map
Here is a sample codesandbox that illustrates the issue...
https://codesandbox.io/s/parsing-mu0c5
I do have the emitDecoratorMetadata option enabled, and still see the issue...
The tests in this project do not test using a class that has properties that are Typed (or exposed).
Here is an example test that depicts the undesired behavior...
it("using Map with custom objects", () => {
class BiggerWeapon {
@Expose({ name: "size" })
size?: string;
@Expose({ name: "model" })
model?: string;
@Expose({ name: "range" })
range?: number;
}
class BiggerUser {
id?: number;
name?: string;
@Type(() => BiggerWeapon)
weapons: Map<string, BiggerWeapon> = new Map();
}
let biggerWeaponDef = {
id: 1,
name: "Max Pain",
weapons: {
firstWeapon: {
model: "knife",
range: 1,
size: "extraLarge"
}
}
};
const biggerWeaponUser = plainToClass(BiggerUser, biggerWeaponDef);
console.log(`biggerWeaponUser`, biggerWeaponUser);
expect(biggerWeaponUser).toBeInstanceOf(BiggerUser);
expect(biggerWeaponUser.weapons).toBeInstanceOf(Map);
expect(biggerWeaponUser.weapons.get("firstWeapon")).toMatchObject({
model: "knife",
range: 1,
size: "extraLarge"
});
expect(biggerWeaponUser.weapons.size).toEqual(1);
});
outputs the following...
expect(received).toEqual(expected) // deep equality
Expected: 1
Received: 3
269 | size: "extraLarge"
270 | });
> 271 | expect(biggerWeaponUser.weapons.size).toEqual(1);
| ^
272 | });
273 | });
274 |
at Object.<anonymous> (tests/__tests__/models/parse.test.tsx:271:43)
console.log tests/__tests__/models/parse.test.tsx:262
biggerWeaponUser BiggerUser {
weapons: Map {
'firstWeapon' => BiggerWeapon { model: 'knife', range: 1, size: 'extraLarge' },
'model' => undefined,
'range' => undefined
},
id: 1,
name: 'Max Pain'
}
The following keys should NOT be in the weapons map...
'model' => undefined,
'range' => undefined
I had a similar issue with classes with Map fields not getting properly reconstructed, although my issue was resolved by noticing that the library only seems to support maps with string keys (I was using a Map<number, T> which would not work).
Also, I seem to have no luck getting the annotations to work for converting field which is a Map of Set objects, like this:
import {serialize, deserialize, Transform, Type} from "class-transformer";
class Foo {
// @Transform((value, obj, type) => {
// console.log('Passed as "value"', value); // Logs plain object
// console.log('Passed inside "obj"', obj); // Logs class
// }, {toClassOnly: true})
@Type(() => Map)
bar: Map<string, Set<number>> = new Map([['baz', new Set([3, 5])]]);
}
const foo = new Foo();
const fooStr = serialize(foo);
const plainFoo = deserialize(Foo, fooStr);
plainFoo.bar.forEach((value, key, map) => {
console.log(value);
map.set(key, new Set(value));
});
console.log('Original class:', foo);
console.log('Original class.bar:', foo.bar); // Map with Set entries
console.log('Serialized class:', fooStr);
console.log('Reconstructed object:', plainFoo);
console.log('Reconstructed object.bar:', plainFoo.bar); // Map with Array entries
No use of the Transform annotation seemed to get me the desired result. Would appreciate if this issue got fixed / or if there was a more clear way to specify nested types using this library.
I have same issue, need support urgently :(
This is a workaround
export class MyObject {
@Transform(value => {
let map = new Map<string, Train>();
for (let entry of Object.entries(value))
map.set(entry[0], plainToClass(Train, entry[1]));
return map;
}, { toClassOnly: true })
trainMap: Map<string, Train>;
}
NOTE: Do NOT add a @Type(()=> Train) on the field, as this causes the @Transform to fail.
@Chriscbr the above might work for you.
Can confirm the issue is still unresolved, but all my thanks to @jamesmikesell , his workaround works perfectly.
To complete workaround of @jamesmikesell, you need to provide another transformation for toPlainOnly:
import {classToPlain, plainToClass, Transform} from 'class-transformer';
import {IsNotEmpty, ValidateNested} from 'class-validator';
export class MyObject {
// @Type(() => Train)
@ValidateNested({each: true})
@IsNotEmpty()
@Transform(value => MyObject.trainsAttributeTransformToClass(value), {toClassOnly: true})
@Transform(value => MyObject.trainsAttributeTransformToPlain(value), {toPlainOnly: true})
readonly trains: Map<string, Train> = new Map();
static trainsAttributeTransformToClass(value: any): Map<string, Train> {
const map: Map<string, Train> = new Map();
if (value && values instanceof Object) {
for (const entry of Object.entries(value)) {
map.set(entry[0], plainToClass(Train, entry[1]));
}
}
return map;
}
static trainsAttributeTransformToPlain(value: any): object {
const trains: {[key: string]: object} = {};
if (value && value instanceof Map) {
for (const entry of value.entries()) {
trains[entry[0]] = classToPlain(entry[1]);
}
}
return trains;
}
}
Also added static functions into class for easy testing.
@marcalj I haven't had any problem when using when using classToPlain with a map, but the completeness is appreciable
@marcalj I haven't had any problem when using when using classToPlain with a map, but the completeness is appreciable
The toPlainOnly It's required if you want to use classToPlain method correctly.
Most helpful comment
To complete workaround of @jamesmikesell, you need to provide another transformation for
toPlainOnly:Also added
staticfunctions into class for easy testing.