typestack / class-transformer

Decorator-based transformation, serialization, and deserialization between objects and classes.
MIT License
6.78k stars 497 forks source link

How to include context in transformations? #900

Open replyqa opened 3 years ago

replyqa commented 3 years ago

Description

Sometimes, transformations require external values. For example, if you have a geopoint field in the class being converted, you can calculate the distance between that geopoint and the geopoint in the context (in nestjs that can be the requesting user's geopoint).

Proposed Solution

This can all be done manually, but if we are going to do this manually, then it defeats the purpose of using class-transformer, especially when used with arrays. Why not include an optional context in the transform options, and then pass it as an argument to the transform function?

The Django way

In Django, a serialization context can be passed that makes this extremely easy. Does class-transformer have such an option?

def get_distance(self, obj):
    destination = obj.geo
    source = self.context.get("geo")
    return get_distance_function(source, destination)
julianpoemp commented 5 days ago

I know this issue is over three years old, but I was facing the same problem and I found a workaround. If anyone knows a better solution, let me know.

You can extend the ClassTransformOptions interface to contain a context:

export interface ClassTransformOptionsWithContext extends ClassTransformOptions {
  context?: unknown;
}

Then whenever a method asked for ClassTransformOptions you can provide ClassTransformOptionsWithContext:

export function serializeData<I>(instance: I, options?: ClassTransformOptionsWithContext): I {
  return instanceToPlain(instance, {
    excludeExtraneousValues: true, // <- example, set here your global TransformOptions
    ...(options ?? {}),
  }) as I;
}

In a transformation you can do this:

export class ExampleClass {
   @Transform(({obj, value, options}: {obj: any, value: number, options: ClassTransformOptionsWithContext}) => { 
      if(options?.context?.replaceNumber) {
        return options.context.replaceNumber;
      }
      return value;
   })
   someNumber: number;

   constructor(partial?: Partial<ExampleClass>){
      if(partial) Object.assign(this, partial);
   }
}

const serialized = serializeData(new ExampleClass({
   someNumber: 123
}); // = {someNumber: 123};

const serialized2 = serializeData(new ExampleClass({
   someNumber: 123
}, {context: { replaceNumber: -123 } }); // = {someNumber: -123};

For NestJS users: You can set a fixed by request context using @SerializeOptions() or you can disable the serialization for the method and use my proposed serializeData() function and return its value. That would allow you to set a context dynamically.