class-transformer
class-transformer copied to clipboard
fix: toClassOnly still expose the name in instanceToPlain
I have a DTO class and I want some specific key to be covert with different name only in instanceToPlain operation
the dto class:
export class CreateEventDto {
@Expose({ toClassOnly: true, name: 'type' })
eventTypeId: number;
}
I can get it to work in plainToInstance for example:
const obj = { type: 2 };
console.log(plainToInstance(CreateEventDto, obj)); // log CreateEventDto { eventTypeId: 2 }
but if I convert it again using instanceToPlain, the result is showing { type: 2 } instead of { eventTypeId: 2 }
const obj = { type: 2 };
const dto = plainToInstance(CreateEventDto, obj);
console.log(dto); // log CreateEventDto { eventTypeId: 2 }
console.log(instanceToPlain(dto)); // log { type: 2 }, but I expect it to be { eventTypeId: 2 }
Is there anything I'm doing wrong? or any proper way to this kind of conversion? Thanks for answering :)
Having this same problem. Is this only happening in newer versions? Is there a version to rollback to that this still works?
Any updates on this? Still seems to be an issue. Is there a consensus on any workarounds for the time being?
This seems to be a bug and needs fix.
Until it is fixed, you could create another getter for toPlainOnly and exclude the current on toPlainOnly.
If I am correct it should work, but I will write an example workaround later.
For a quick (but not nice) fix you could do
export class CreateEventDto {
@Expose()
@Exclude( { toPlainOnly: true })
type: number;
@Expose()
get eventTypeId() {
return this.type;
}
}
@diffy0712 Thanks for the response and the temporary workaround. This will unfortunately be too verbose as I have a ton of exposed properties currently that are toClassOnly
as I am importing data from another system and want it permanently in my format instead.
My Temporary Solution
Not sure if its relevant or if this is a correct solution but for my situation, I am using NestJS
and the issue arises when I send data back to the client since its transforming my models to plain before sending. My temporary workaround that seems to be working is as follows. Posting incase it helps anyone else or you have any feedback for the time being on why I shouldn't do it this way haha. The super.transformToPlain(plainOrClass, options);
call I believe is just a direct call to the instanceToPlain()
method but wrapped by nestjs. https://github.com/nestjs/nest/blob/master/packages/common/serializer/class-serializer.interceptor.ts#L84
Edited I noticed my temporary workaround wasn't working with nested objects so I implemented a recursive solution to hopefully fix all of those correctly as well.
import {
ClassSerializerContextOptions,
ClassSerializerInterceptor,
NestInterceptor,
PlainLiteralObject,
} from '@nestjs/common';
import { defaultMetadataStorage } from 'class-transformer/cjs/storage.js';
import type { MetadataStorage } from 'class-transformer/types/MetadataStorage';
/**
* This serializer is being used to fix an issue with the class-transformer library where
* instanceToPlain is not abiding by certain expose flags. This aims to remedy incorrectly keyed properties
* when specified as toClassOnly.
*
* Once the issue is resolved in the package, this should be able to be removed.
* @see https://github.com/typestack/class-transformer/issues/1324
*/
export class AppClassSerializerInterceptor
extends ClassSerializerInterceptor
implements NestInterceptor
{
/** Load in the default local storage... I realize this isn't supposed to be public and used */
private readonly metadata: MetadataStorage = defaultMetadataStorage;
override transformToPlain(
plainOrClass: any,
options: ClassSerializerContextOptions,
): PlainLiteralObject {
// Perform normal transform. This will have incorrect keys as it will have ignored the toClassOnly
const result = super.transformToPlain(plainOrClass, options);
// Recursively fix the result object and nested class objects
this.fixExposedKeys(plainOrClass, result);
return result;
}
/**
* Traverse the resulting object and compare it with class instance version. Patch the result model recursively
* checking nested objects.
*/
private fixExposedKeys(plainOrClass: any, result: PlainLiteralObject) {
// Get all the fields that are exposed and their options for the target class
const exposed = this.metadata.getExposedMetadatas(plainOrClass.constructor);
const toRevert = new Map<string, string>();
// Cache this classes exposed properties to lookup
for (const field of exposed) {
if (
field.options.toClassOnly &&
field.options.name &&
field.propertyName
) {
toRevert.set(field.options.name, field.propertyName);
}
}
for (const key in result) {
const val = result[key];
const correctKey = toRevert.get(key) ?? key;
// Convert the keys back to the originals and delete the incorrectly converted ones.
if (correctKey !== key) {
result[correctKey] = val;
delete result[key];
}
// We need to also fix nested models...
if (val && typeof val === 'object') {
this.fixExposedKeys(plainOrClass[correctKey], val);
}
}
}
}
Thank you for sharing your solution. I think it looks alright and a pretty nice workaround.