class-transformer
class-transformer copied to clipboard
question: how to use exposing method inside a nested object array dto?
Hello,
i was trying to serialize a Dto in a jest test, which contains a nested array of another dto.
import { Expose, Type, instanceToPlain } from "class-transformer";
import "reflect-metadata";
export class AlbumDto {
id: number;
name: string;
@Type(() => PhotoDto)
photos: PhotoDto[];
// here are not any issues
@Expose({ name: "title" })
getTitle(): string {
return "My super album";
}
constructor(partial: Partial<AlbumDto>) {
Object.assign(this, partial);
}
}
export class PhotoDto {
@Expose({ name: "file" })
filename: string;
constructor(partial: Partial<PhotoDto>) {
Object.assign(this, partial);
}
@Expose({ name: "author" })
public getAuthor(): string {
return "John";
}
}
describe("transmission detail response dto serialization test", () => {
it("learning test nested objects", () => {
const albumPartial = {
id: 1,
name: "My Album",
photos: [{ filename: "photo1.jpg" }, { filename: "photo2.jpg" }],
};
const photoPartial = {
filename: "somephoto.jpg",
};
// this results into an error
const album = new AlbumDto(albumPartial);
const photo = new PhotoDto(photoPartial);
console.log(instanceToPlain(photo));
console.log(instanceToPlain(album));
});
});
The problem:
I receive the following error:
const album = new AlbumDto(albumPartial);
The error:
src/transmission/dtos/transmission-detail-response.dto.spec.ts:50:32 - error TS2345: Argument of type '{ id: number; name: string; photos: { filename: string; }[]; }' is not assignable to parameter of type 'Partial<AlbumDto>'.
Types of property 'photos' are incompatible.
Type '{ filename: string; }[]' is not assignable to type 'PhotoDto[]'.
Property 'getAuthor' is missing in type '{ filename: string; }' but required in type 'PhotoDto'.
50 const album = new AlbumDto(albumPartial);
~~~~~~~~~~~~
src/transmission/dtos/transmission-detail-response.dto.spec.ts:31:10
31 public getAuthor(): string {
~~~~~~~~~
'getAuthor' is declared here.
Here iam a little bit confused, why this error is happening for the nested dto? - it is also instantiated via a partial! If i remove the getAuthor
method from the PhotoDto
everything is fine. So on the first level there are no issues adding methods. But i would also not expect any issues in the nested levels, because it is just a partial i need to pass to the constructor right? So property name
would be enough in the albumPartial.photos
objects.
How to solve the issue in the best way? I know that i can also instantiate new PhotoDto
in the album like this:
const albumPartial = {
id: 1,
name: "My Album",
photos: [
new PhotoDto({ filename: "photo1.jpg" }),
new PhotoDto({ filename: "photo2.jpg" }),
],
};
const album = new AlbumDto(albumPartial);
But this looks ugly and enforces me to do this for every nested object in my albumPartial
. Not to imagine having nested in nested Objects.
Any solutions or ideas?
Thanks and best regards
Hi @andreasvh-conceto, Thank you for the detailed issue and sorry for a late reply.
The error you got is a typescript error and nothing to do with this library.
The error clearly states that 'Types of property 'photos' are incompatible.' You used the Partial<AlbumDto>
, but AlbumDto contains 'photos: PhotoDto[]', which is not Partial, so when you instantiate with an object containing photos
, it will require you to be compatible with 'PhotoDto'. (so Partial<AlbumDto> is not a deep Partial).
But this is not the way you would want to use class-transformer
. Here is a snippet for your scenario which will work:
class AlbumDto {
id: number;
name: string;
@Type(() => PhotoDto)
photos: PhotoDto[];
@Expose()
get title(): string {
return "My super album";
}
}
class PhotoDto {
@Expose()
filename: string;
@Expose()
get author(): string {
return "John";
}
}
describe("transmission detail response dto serialization test", () => {
it("learning test nested objects", () => {
const albumPartial = {
id: 1,
name: "My Album",
photos: [{ filename: "photo1.jpg" }, { filename: "photo2.jpg" }],
};
const album = plainToInstance(AlbumDto, albumPartial);
expect(album.id).toEqual(1);
expect(album.name).toEqual('My Album');
expect(album.photos[0].filename).toEqual('photo1.jpg');
expect(album.photos[1].filename).toEqual('photo2.jpg');
expect(instanceToPlain(album)).toEqual({
id: 1,
name: 'My Album',
photos: [
{ filename: 'photo1.jpg', author: 'John' },
{ filename: 'photo2.jpg', author: 'John' }
],
title: 'My super album'
});
});
});
Thank you for the detailed explanation. This helps! Best regards :)
Hi @diffy0712
one question came into my mind, while playing around with your answer. When i choose to @Expose({name: "myPropertyName"})
the plainToInstance
method returns undefined for the annotated property. Currently i handle that using the ignoreDecorators: true
option like this:
const album = plainToInstance(AlbumDto, albumPartial, {
ignoreDecorators: true, // this prevents that the property will be undefined, since it is keeping the property name from the created instance
});
Is this the right approach to go for?
Thanks a lot and best regards
Hello @andreasvh-conceto,
if you provide @Expose({name: "myPropertyName"})
decorator to the name
property, when calling plainToInstane
, it will expect your plain to have a property called 'myPropertyName' instead of the real property name
, which I assume you did not provide in your plain object, so it was set to undefined.
I would not suggest to use ignoreDecorators
for this, as it is the same as not using the decorator in your situation.
So updating my previous example:
// if you add
@Expose({name: "myPropertyName"})
name: string;
// then you plain would needs to change to
const albumPartial = {
id: 1,
myPropertyName: "My Album",
photos: [{ filename: "photo1.jpg" }, { filename: "photo2.jpg" }],
};
NOTE: if you override the name in expose, then you will get that name in your instanceToPlain
result as well. If you need to override name only on one way please use the options toPlainOnly
or toClassOnly
. @Expose({name: "myPropertyName", toClassOnly: true})
Please read the documentation a bit more in detail, because everything I have wrote here is already in the documentations!
Hope this helps.