Home > Net >  Validate nested DTO objects using class validator
Validate nested DTO objects using class validator

Time:06-07

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.

  • Related