Class transformer not working with swc
DanielRamosAcosta opened this issue ยท 16 comments
Describe the bug
When applying the plainToClass
function to a class defined with class-validator
, it returns the class with no attributes
Input code
Here is a repo with a test reproducing the error
const usersOptionsInput = plainToClass(GetUsersOptionsInput, {
query: "Query",
pagination: { skip: 0, limit: 20 },
});
expect(usersOptionsInput.query).toEqual("Query"); // error
Config
You can also find the config at the repo reproducing the error
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2018"
},
"module": {
"type": "commonjs",
"noInterop": true
}
}
Expected behavior
I would expect the instance of the class to have all the defined attributes.
Version
The version of @swc/core: 1.2.80
Additional context
Just one more time, I created this repo in order to reproduce the error
This error is related to #1362
I'm coming from #1362 and this doesn't work #1362 (comment)
Good repro btw @DanielRamosAcosta!
I've been checking the generated code, and there's a problematic part.
Minimal repro
- Source
import { IsOptional, IsString } from 'class-validator';
export class GetUsersOptionsInput {
@IsString()
@IsOptional()
public query?: string;
// it works
// public query?: string = undefined;
}
- Generated
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.GetUsersOptionsInput = void 0;
var _classValidator = require("class-validator");
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
var desc = {
};
Object.keys(descriptor).forEach(function(key) {
desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;
if ("value" in desc || desc.initializer) {
desc.writable = true;
}
desc = decorators.slice().reverse().reduce(function(desc, decorator) {
return decorator ? decorator(target, property, desc) || desc : desc;
}, desc);
if (context && desc.initializer !== void 0) {
desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
desc.initializer = undefined;
}
// IF NO INITILIZAER, THE RETURNED DESCRIPTOR IS NULL
if (desc.initializer === void 0) {
Object.defineProperty(target, property, desc);
desc = null;
}
return desc;
}
function _initializerDefineProperty(target, property, descriptor, context) {
if (!descriptor) return;
Object.defineProperty(target, property, {
enumerable: descriptor.enumerable,
configurable: descriptor.configurable,
writable: descriptor.writable,
value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
});
}
var _class, _descriptor, _dec, _dec1, _dec2;
let GetUsersOptionsInput = ((_class = class GetUsersOptionsInput1 {
constructor(){
_initializerDefineProperty(this, "query", _descriptor/*THIS DESCRIPTOR IS NULL*/, this);
}
}) || _class, _dec = (0, _classValidator).IsString(), _dec1 = (0, _classValidator).IsOptional(), _dec2 = typeof Reflect !== "undefined" && typeof Reflect.metadata === "function" && Reflect.metadata("design:type", String), _descriptor = _applyDecoratedDescriptor(_class.prototype, "query", [
_dec,
_dec1,
_dec2
], {
configurable: true,
enumerable: true,
writable: true,
initializer: void 0 // THE INITIALIZER IS UNDEFINED
}), _class);
exports.GetUsersOptionsInput = GetUsersOptionsInput;
This is the problematic part from _applyDecoratedDescriptor
, this branch is executed by initializer: void 0
if (desc.initializer === void 0) {
Object.defineProperty(target, property, desc);
desc = null;
}
When the property decorated has not initializer
the descriptor decorator returned is null
. If we remove that if
, the repro code works and tests pass.
I don't know exactly why that check is neccesary, but there is the problem.
I hope it helps @kdy1! Excellent project btw ๐
Great work @tonivj5! i tried diving into the generated code but I didn't reach any conclusions, thanks for your insight!
Issue on class-transformer
side typestack/class-transformer#796 I suppose invalid, and it should be fixed in swc.
@unlight if class-validator works perfect with tsc and fail with swc. I think it's a problem of swc handling decorators...
@kdy1 yep! It doesn't work.
This code does not work:
import "reflect-metadata";
import { IsOptional, IsString } from "class-validator";
export class GetUsersOptionsInput {
@IsString()
@IsOptional()
public query?: string;
}
This code does work:
import "reflect-metadata";
import { IsOptional, IsString } from "class-validator";
export class GetUsersOptionsInput {
@IsString()
@IsOptional()
public query?: string = undefined;
}
I did a deeper research here #2117 (comment)
+1, it would be great to fix this. We use class-validator
pretty heavily, and we would like to run our unit tests with ts-node + swc
since it's dramatically faster than using the default transpiler with ts-node
.
If you're struggling with this issue here is the way to fix it so it's almost 0 pain while we're waiting for fix.
nestjs/nest-cli#731 (comment)
this is blocking for us to migrate to swc, company wide
@acoroleu-tempus use the workaround I mentioned above.
I investigated this.
For the initializer
property of the property descriptor for fields without an initializer, babel uses null
while swc emits void 0
. _applyDecoratedDescriptor
returns null
if initializer
is undefined
, so the property descriptor used for _initializerDefineProperty
while instantiating a class is an object for babel while being null
for swc.
I tried changing the value to null
, but it broke other tests. Namely, the tests I added for #2127 in #3105 was problematic.
I decided to postpone this.
Coming from: https://stackoverflow.com/questions/70956406/plaintoclass-from-class-transform-converts-incorrecly
Any progress or possible workaround on this issue? Tried to use esbuild
and plainToClass
method works correctly.
This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.