Search code examples
jsontypescriptclass-transformer

Serialization and deserialization in Typescript for classes with nested maps


I have some TypeScript classes with fields that are Map objects containing other objects. I'm using the class-transformer package to serialize and deserialize (to and from JSON), but I'm having trouble getting it to work.

Additionally, a field in the parent class is not being correctly transformed into its intended type during deserialization (see the "created" field which is NOT transformed into Date).

The documentation for class-transformer seems either outdated or lacks guidance for handling this specific scenario.

Here is a examples of the classes:

// Type to be used for Unique Identifiers (UID).
export type Uid = string;

export class UidObject {
    // Unique identifier of the object.
    readonly uid: Uid;

    constructor(uid: Uid) {
        this.uid = uid;
    }
}

export class TimeStampedObject extends UidObject {
    @Type(() => Date)
    readonly created: Date;

    constructor(uid: Uid, created: Date) {
        super(uid);
        this.created = created;
    }
}

export class Profile extends TimeStampedObject {

    /// User display alias.
    readonly alias: string;

    constructor(
        uid: Uid,
        created: Date,
        alias: string,
    ) {
        super(uid, created);
        this.alias = alias;
    }
}

export class Group extends UidObject {

    readonly name: string;

    @Type(() => Map) // What should be used here? 
    readonly profiles: Map<string, Profile>;

    constructor(
        uid: Uid,
        name: string,
        profiles: Map<string, Profile>,
    ) {
        super(uid);
        this.name = name;
        this.profiles = profiles;
    }
}

Using the "class-transformer" functions does not seem to convert the "profiles" field into a Map<Uid, Profile> but just to Map<string, any>.

let profiles = new Map();
profiles.set("uid-prof-1", new Profile("uid-prof-1", new Date(), "alias-prof-1"));
profiles.set("uid-prof-2", new Profile("uid-prof-2", new Date(), "alias-prof-2"));

let group = new Group("uid-coco", "Coco", profiles);

let json = instanceToPlain<Group>(group);
let instance = plainToInstance<Group, any>(Group, json);

console.log(group);
console.log("------ After =>");
console.log(instance); // This shows an objets which is not in the correct format.

Solution

  • I implemented a solution based on this old workaround by creating a custom decorator to handle the transformation of nested Map objects.

    export function MapTransform<V>(cls: ClassConstructor<V>): PropertyDecorator {
        return Transform(({ value }: TransformFnParams) => {
            const map = new Map<string, V>();
            for (const [key, val] of Object.entries(value)) {
                map.set(key, plainToInstance(cls, val));
            }
            return map;
        }, { toClassOnly: true });
    }
    

    And apply the @MapTransform decorator to the profiles field in your Group class:

    export class Group extends UidObject {
        readonly name: string;
    
        @MapTransform(Profile) // Use the custom decorator here
        readonly profiles: Map<string, Profile>;
    
        constructor(
            uid: Uid,
            name: string,
            profiles: Map<string, Profile>,
        ) {
            super(uid);
            this.name = name;
            this.profiles = profiles;
        }
    }