class-transformer: discussion: poor handling of errors thrown by target class constructors (like Firebase Timestamp)

I’ve spent some time figuring this problem out, and since the repo isn’t being actively maintained right now I figure the best I can do to give other’s access to my findings is to put them in an issue.

In TransformOperationExecutor.ts (.js if you are digging through the compiled code from node_modules like I was), the library tries to figure out how to construct it’s target type, and under certain conditions will reach line 94:

newValue = new (targetType as any)();

(or line 86 in the JS version):

newValue = new targetType();

This just blindly calls the target type’s constructor with no arguments, which works for classes that don’t have real constructors that expect arguments, but doesn’t work for classes that have constructors that validate their arguments at runtime.

I think this is probably what the code should be doing in this case, there’s no way for it to figure out if the constructor expects arguments, or what it does with them, and it works for many use cases. But there are plenty of cases where it won’t work, and those can be handled in a way that will at least give more useful errors.

The Firebase/Firestore SDK’s are switching to using their own Timestamp class instead of the native Date class for storing time values, and the constructor to that class expects two numbers, and throws a runtime exception if they are not present and valid.

Ideally, TransformOperationExecutor would wrap the call to the constructor in a try-catch block, so it could print a helpful error message like: “the constructor for the target class threw an error, this class may not be compatible with class-transformer…” then re-throw the caught error. This would make it much more obvious what is going on, and wouldn’t change the runtime behavior at all, a strict improvement over the current functionality.

I would gladly submit a PR for this, but PR’s aren’t getting reviewed right now so I’m not going to invest my time in that.

For anyone else that runs into this compatibility issue, or something similar, I have found a fairly respectable workaround.

export class User {
  @IsString()
  @IsEmail()
  email!: string;

  @Transform((value: any) => {
    const seconds: number = value._seconds;
    const nanoseconds: number = value._nanoseconds;
    return new Timestamp(seconds, nanoseconds);
  })
  @Type(() => Object)
  created_time!: Timestamp;
}

This example is for the specific types I was working with, but the pattern can be used in many other cases.

I use @Type(() => Object) to bypass the call to the Timestamp constructor, class-transformer will just give me a plain object copy of the source data for that key, Then I use @Transform to hook into the transformation lifecycle, and craft a valid call to the constructor out of the Object.

I’ve seen a few open issues in this repo that could potentially be helped by this pattern, it may be a good idea to add a note about it to the readme, or add a new decorator that accomplishes the same thing, giving people a simpler way to write their own custom transformers.

I appreciate how useful this library is, combined with class-validator it lets me easily add runtime-validation of dynamic data without having to write a ton of custom code, it’s just unfortunate that it isn’t being maintained well right now.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 6
  • Comments: 16 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Firebase is a hot mess.

Yes, yes it is. It’s an amazing platform and I happily use it in several production applications, but like so many google products, it is surprisingly janky.

This is most bizarre. I have this following code:

const data: UserDTO = plainToClass(UserDTO, { 
  email: 'a@b.com', 
  myDate: new firebase.firestore.Timestamp(1548955423,0)
});
console.log(data);

with UserDTO

export class UserDTO {

  email: string;

  @Transform(DateUtil.firebaseFirestoreDateConversion)
  myDate: Date;

}

Nevermind what is happening in the transformation function. The point is that :

  • this code executed in web browser environment (angular app, ionic app), converts the Timestamp just fine - conversion function expects timestamp, converts it to Date and all is good. Constructor for Timestamp is somehow not called.
  • this same code executed as Firebase function (in their environment), crashes (it never enters the transformation function). Unfortunately, it is very hard to debug that environment, so I don’t know what’s going on in detail, but it is definitely connected to Timestamp. I assume the constructor is actually called and there it crashes. So in Firebase functions environment, I have to use the workaround provided by @YGKtech .

Granted, firebase functions and web page doesn’t share exactly the same firebase/firestore functions (they use different libs), but something is very weird here. It’s as if it was reliant on something in environment (shims?) or in actual node.js.

@YGKtech: That be great. The description sounds interesting. Feel free to ping me when you are ready to merge.

@cojack : just got caught up on the status of the repo, if you and the other new maintainers want to prioritize other fixes, I could try setting up a PR for this sometime in the next week or two.