fix: When I use `plainToClass` in my nestjs app to validate nested env it cannot convert them
Description
I am using class-validator and class-transformer to validate my .env but it cannot validate nested objects even though I am using @type(() => ClassName) in my code. But at the same time I am able to validate incoming http request with it. I mean I created a dummy endpoint which receives an object with a nested object inside it and it works just fine. IDK what i am doing wrong. I really appreciate your helps.
Minimal code-snippet showcasing the problem
https://github.com/kasir-barati/bugs/tree/class-transformer-bug
Clone the repo and follow the instructions in the readme.md
Expected behavior
Validate the .env successfully and not log the value of KEYCLOAK_SECRET like a string.
Actual behavior
It cannot convert the type KEYCLOAK_SECRET to it's respective type as I code.
There is no bug in class-transformer/class-validation. The problem is with your configuration.
- Your env (KEYCLOAK_SECRET) is a JSON, so you need to parse is into object. You can do this by adding
Transform
class EnvironmentVariables {
@ValidateNested()
@Transform(({ value }) => JSON.parse(value)) // <- this line of code
@Type(() => KeycloakSecretValidator)
KEYCLOAK_SECRET: KeycloakSecret;
}
-
process.envcontains not only properties from.envbut much more so you have two solutions:
- Set
forbidUnknownValuesasfalse
const validatedConfigsErrors = validateSync(validatedConfigs, {
skipMissingProperties: false,
forbidUnknownValues: false, // <- this line
forbidNonWhitelisted: true,
});
- Or add
exposeand remove all unknown properties during parsing to class:
class EnvironmentVariables {
@ValidateNested()
@Transform(({ value }) => JSON.parse(value))
@Type(() => KeycloakSecretValidator)
@Expose() // < -- this line of code
KEYCLOAK_SECRET: KeycloakSecret;
}
const validatedConfigs = plainToClass(EnvironmentVariables, config, {
enableImplicitConversion: true,
excludeExtraneousValues: true, // <- this line of code
});
After this changes it should work
Thanks for the clarification, but still I cannot stop wondering why in the auth.config.ts I have to go through that @Transform(({ value }) => JSON.parse(value)) and in t.dto I do not need to do that?
Though I think basically my global ValidationPipe in main.ts is the same as what I did at auth.config.ts, are not they?
Because nestjs have built in parser from JSON to plain JS object when request comes from API (via controller). So nestjs does it but under the hood.
When you load data to app via env, nestjs parser is not run, so you have to do it manually.
app.useGlobalPipes(
new ValidationPipe({
transformOptions: {
enableImplicitConversion: false,
},
}),
);
This global pipe only touch HTTP requests, not all used of class-transform/class-validation
This global pipe only touch HTTP requests, not all used of class-transform/class-validation
I knew this, but I did not know about the builtin parser. Thanks a lot. Now everything is clear as day to me.
Question--
Is it not possible to get the exact list of properties from the class itself, and to tell plainToClass() to only parse those properties? Why is it necessary to add Expose() to each property? On the surface, seems redundant.
Nope, it is not possible. Properties declarations only exist in TS. Library has to somehow get those information and is does... but from Expose decorator.
You can read more about it in README.md