Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent some fields of my object to be transformed by class-transformer

I'm trying to use the library class-transformer(https://github.com/typestack/class-transformer) to serialize typescript classes to firebase, which doesn't accept customs types.

The thing is some of the data are actually data compatible with firebase, like "coordinates" being in the GeoPoint format.

I've this class being serialized:

export abstract class StopStep extends Step {
  id: string;
  name: string;
  description: string;
  pointOfInterest: PointOfInterest | null;
  address: string;
  coordinates: firebase.firestore.GeoPoint;
}

I would like to prevent the "coordinates" field to be transformed. It should be present, just not untransformed.

I tried to play with the @Transform(({value})=>...), but it appears it's only an "additional" transformation, it doesn't allow me to just keep the same format.

I also tried Exclude, but then the field isn't present anymore.

Is there a way to accomplish this with this library?

like image 601
J4N Avatar asked Mar 16 '21 20:03

J4N


1 Answers

Answer: I've analyzed and debugged the whole TransformOperationExecutor.ts, and simply put, there is no in-build way to suppress the myriad of transformations this executor attempts to do. I thought maybe removing the @Type decorator all together could achieve the goal. But even with the missing type information, the library still tries to transform complex objects into plain objects as best as possible. The reason why @Transform(({value})=>...) is not working is that even for the returned value, there are multiple checks to figure out if the transformed value needs any further transformations to be "plain".

Workaround: The only solution I see is to build that functionality yourself. I can offer the following implementation to simulate an @Ignore feature:

import { classToPlain, ClassTransformOptions, Transform, TransformFnParams } from 'class-transformer';

// New decorator to be used on members that should not be transformed.
export function Ignore() {
    // reuse transform decorator
    return Transform((params: TransformFnParams) => {
        if (params.type == TransformationType.CLASS_TO_PLAIN) {
            // class-transformer won't touch functions,
            // so we use function-objects as container to skip transformation.
            const container = () => params.value;
            container.type = '__skipTransformContainer__';

            return container;
        }
        // On other transformations just return the value.
        return params.value;
    });
}

// Extended classToPlain to unwrap the container objects
export function customClassToPlain<T>(object: T, options?: ClassTransformOptions) {
    const result = classToPlain(object, options);
    unwrapSkipTransformContainers(result);
    return result;
}

// Recursive function to iterate over all keys of an object and its nested objects.
function unwrapSkipTransformContainers(obj: any) {
    for (const i in obj) {
        if (obj.hasOwnProperty(i)) {
            const currentValue = obj[i];
            if (currentValue?.type === '__skipTransformContainer__') {
                // retrieve the original value and also set it.
                obj[i] = currentValue();
                continue;
            }

            // recursion + recursion anchor
            if (typeof currentValue === 'object') {
                unwrapSkipTransformContainers(currentValue);
            }
        }
    }
}

This solution exploits that the class-transformer won't transform functions (except get/set functions), as those are not part of a plain object. We reuse the Transform decorator to wrap members we don't want to be transformed with a function. We also mark those functions with the string __skipTransformContainer__ to make it easy to find them later and not accidentally call the wrong functions. Then we write a new customClassToPlain, which first calls the default classToPlain and then calls a recursive unwrap function on the result. The unwrap function is called unwrapSkipTransformContainer and recursively searches all __skipTransformContainer__-containers to retrieve the untransformed values.

Usage:

export abstract class StopStep extends Step {
  id: string;
  name: string;
  description: string;
  pointOfInterest: PointOfInterest | null;
  address: string;
  @Ignore()
  coordinates: firebase.firestore.GeoPoint;
}

class ExampleStep extends StopStep {/*logic*/}
    
const step = new ExampleStep();
const plain = customClassToPlain(step);
like image 168
Mirco S. Avatar answered Oct 12 '22 10:10

Mirco S.