4

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))
  }

1 Answer 1

6
+50

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I try to keep opinions out of answers, but this is one of many reasons why I think decorated classes are a bad mechanism for representing payloads. We use JSON for payloads because it's readable and allows us to data in the same form as it's structure. JSON = "JavaScript Object Notation", so we're using JavaScript notation in our serialized payloads, but then NOT using JavaScript notation in our... JavaScript. :-/
Hi Eric, Thank you for the answer! It indeed fixed the problem. I like the decorated classes because it forces a certain structure for the payload. When combining it with Typescript it also helps when working with the DTO objects. But I am still searching for the proper way to setup my projects. I will award you the bounty within 7 hours.
You didn't really specify how you're setting up your project, but generally those who lean towards decorated classes for payloads also lean towards decorated classes for setting up the API as well. Frameworks like Nestjs go full-scale in this direction, but there are more direct approaches like github.com/w3tecch/express-typescript-boilerplate.
By the above, I just meant that there are other ways to enforce structure. In class based languages like Java, it's really the only way to do it. TS is more expressive, though. The payload is a single conceptual object, but classes force you to chop it up into a a series of flattened pieces that no longer look like the actual shape of the payload. Personally, I prefer something like npmjs.com/package/zod because the structure of the models matches that of the payload. To each their own, though. ;-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.