I'm trying to use class validator in order to validate incoming data. The data consists of an array of object. Each object should be validated.
The problem that I am facing is that I keep on getting errors when everything is being input correctly. It seems that the parent class is being checked with it's children's properties and so whitelistValidation
error is being thrown for each property of the child.
This is the error that is being generated:
[
{
"target":{
"drainPoints":[
{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
}
]
},
"value":[
{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
}
],
"property":"drainPoints",
"children":[
{
"target":[
{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
}
],
"value":{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
},
"property":"0",
"children":[
{
"target":{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
},
"value":"roundsurface",
"property":"drainPointType",
"constraints":{
"whitelistValidation":"property drainPointType should not exist"
}
},
{
"target":{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
},
"value":"normal",
"property":"flowType",
"constraints":{
"whitelistValidation":"property flowType should not exist"
}
},
{
"target":{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
},
"value":0.5,
"property":"flowCoefficient",
"constraints":{
"whitelistValidation":"property flowCoefficient should not exist"
}
},
{
"target":{
"drainPointType":"roundsurface",
"flowType":"normal",
"flowCoefficient":0.5,
"point":{
"x":0,
"y":0
}
},
"value":{
"x":0,
"y":0
},
"property":"point",
"constraints":{
"whitelistValidation":"property point should not exist"
}
}
]
}
]
}
]
The DTO object that contains the array:
export class CreateDrainPointDTO extends DTO {
@IsArray()
@IsNotEmpty()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => DrainPointDTO)
drainPoints: DrainPoint[]
}
The Object itself:
export class DrainPointDTO {
@IsString()
@IsOptional()
uuid: string
@IsEnum(DrainPointType)
@IsNotEmpty()
drainPointType: DrainPointType
@IsEnum(DrainPointflowType)
@IsNotEmpty()
flowType: DrainPointflowType
@IsArray()
@IsNotEmpty()
point: Point
@IsNumber()
@IsOptional()
flowCoefficient: number
}
My custom DTO abstract class:
export abstract class DTO {
static async factory<T extends DTO>(Class: new () => T, partial: Partial<T>): Promise<T> {
const dto = Object.assign(new Class(), partial)
const errors = await validate(dto, { whitelist: true, forbidNonWhitelisted: true })
if (errors.length > 0) {
throw new CustomError()
.withError('invalid_parameters')
.withValidationErrors(errors)
}
return dto
}
}
I use this DTO abstract class in order to have a clean way of checking the body inside the controller:
async createDrainPoint (req: Request, res: Response): Promise<void> {
const dto = await DTO.factory(CreateDrainPointDTO, req.body as Partial<CreateDrainPointDTO>)
const drainPoints = await this.drainPointService.create(dto)
res.status(201).json(DrainPointTransformer.array(drainPoints))
}
CodePudding user response:
The problem is that the way you are creating the data doesn't actually end up with instances of DrainPointDTO
in the array, merely objects that conform to its shape. You can see this with the following:
async createDrainPoint (req: Request, res: Response): Promise<void> {
const dto = await DTO.factory(CreateDrainPointDTO, req.body as Partial<CreateDrainPointDTO>)
console.log(dto.drainPoints[0].constructor);
It will output [Function: Object]
, rather than [class DrainPoint]
.
You're creating an instance of CreateDrainPointDTO
, and Object.assign
is filling in the values, but it doesn't map any of the nested values to class instances. It is functionally identical to:
const dto = new CreateDrainPointDTO()
dto.drainPoints = [
{
uuid: 'some-id',
drainPointType: DrainPointType.roundsurface,
flowType: DrainPointflowType.normal,
flowCoefficient: 0.5,
point: {
x: 0,
y: 0,
},
},
]
// outputs '[Function: Object]', NOT '[class DrainPoint]'
console.log(dto.drainPoints[0].constructor)
Since all of the decorators are part of the DrainPointDTO
class, it can't figure out how to validate it. class-validator
is usually used in conjunction with class-transformer (I assume you already have it, because it's where the Type
decorator comes from). It will create instances of the nested classes (as long as they specify the class in the @Type
, which you already did). For your factory, you can instead do:
import { plainToInstance } from 'class-transformer';
// ...
const dto = plainToInstance(Class, partial);
Note: You probably don't want the factory to accept a Partial<T>
, but rather just T
. If the values you're using to initialize the class are missing any that are required, it's not going to be valid anyway. It doesn't really matter for req.body
, as it's of type any
anyhow, but if you were to use it to create one programmatically, it will give you static checking for missing fields.