Unable to validate a JSON object using class-validator in Nest.js

718 views Asked by At

I am trying to validate a POST request coming in to my NestJS application. I am using class-based validation. This is something where I am currently stuck. I am not able to validate registrations property that has a value of JSON.

Here's my DTO:

import { Type } from 'class-transformer';
import {
  IsNotEmpty,
  IsString,
  ValidateNested,
  IsInt,
  IsObject,
} from 'class-validator';

class User {
  @IsNotEmpty()
  @IsString()
  name: string;

  @IsNotEmpty()
  @IsString()
  uid: string;

  @IsNotEmpty()
  @IsInt()
  age: number;
}

export class CreateUserDto {
  @IsObject()
  @ValidateNested()
  @Type(() => User)
  @IsNotEmpty()
  registrations: Record<string, User>;
}

This is giving me an error:

{
  "message": [
    "registrations.name must be a string",
    "registrations.name should not be empty",
    "registrations.uid must be a string",
    "registrations.uid should not be empty",
    "registrations.age must be an integer number",
    "registrations.age should not be empty"
  ],
  "error": "Bad Request",
  "statusCode": 400
}

for JSON data submitted as:

{
  "registrations": {
    "user1": {
      "name": "Prathamesh",
      "uid": "user1",
      "age": 28
    },
    "user2": {
      "name": "Pradnya",
      "uid": "user2",
      "age": 25
    }
  }
}

In the above example, there are 2 user registrations. But can have any number of users' entries.

How to validate something is of type Record?

1

There are 1 answers

1
Moti On

@Type decorator

There is a problem with @Type decorator. With its usage you're basically telling that property registrations is of type User.

Quote from the docs:

When you are trying to transform objects that have nested objects, it's required to know what type of object you are trying to transform. Since Typescript does not have good reflection abilities yet, we should implicitly specify what type of object each property contains. This is done using @Type decorator.

class-validator

The class-validator package is currently not well suited for validating classes with dynamically changing content.

Remember that to validate nested objects they need to be an instance of a class (in this case User), so possible workaround would be to create your own validator because you do not know how many users there will be, e.g.

@ValidatorConstraint()
export class RecordValidator<T extends object>
  implements ValidatorConstraintInterface
{
  validationErrors: ValidationError[] = [];

  validate(
    record: Record<string, T>,
    validationArguments: ValidationArguments,
  ) {
    const ClassTransformer = validationArguments
      .constraints?.[0] as ClassConstructor<T>;
    if (!ClassTransformer) {
      return false;
    }

    const values = Object.values(record);
    this.validationErrors = values
      .map((value) => {
        const transformedValue = plainToInstance(ClassTransformer, value);
        return validateSync(transformedValue);
      })
      .reduce((acc, curr) => [...acc, ...curr], []);

    return this.validationErrors.length === 0;
  }

  defaultMessage() {
    return this.validationErrors.join(', ');
  }
}

export class CreateUserDto {
  @IsObject()
  @Validate(RecordValidator<User>, [User])
  @IsNotEmpty()
  registrations: Record<string, User>;
}

Note: Customize as it as you want, you could also write async validators, but I found it faster to implement it without async

Recommended

I do not understand why you are passing it as a record with unlimited users.

I would recommend using array and it is easily supported out of the box by class-validator