Search code examples
javascripttypescriptvalidationnestjsclass-validator

How to validate nested array of multiple types using class-validator?


I use nestjs with class-validator, and I have the following case:

  • Two classes with same field name that has different types:
class A {
  @IsDefined()
  field: number;
}

class B {
  @IsDefined()
  field: string;
}
  • Third class that has array of elements from types A or B, my problem is how to validate each array member against its own class:
class C {
  @ValidateNested({ each: true })
  @Type(I_DON'T_KNOW_WHAT_TO_DO_HERE)
  array: (A | B)[];
}

How can I validate the array?

Thanks ahead!


Solution

  • You should have a descriminative field, to allow the class-transformer to identify the type to cast and use in the validation. The same can be done with nested objects.

    In the example below, if the property "version" is equal to "v1", then the field should be a number, in the other hand, if the property version is equal to "v2" the property field should be a string.
    In the discriminator property you should set a field to be used to identify the different polymorphic forms of the nested objects. The subTypes.[].value is the class to be used in validation and subTypes.[].name is the value that the discriminator field should have to assume the class added in the subTypes.[].name field.

    import { Type } from 'class-transformer';
    import { IsDefined, IsEnum, IsNumber, IsString, ValidateNested } from 'class-validator';
    
    enum VersionEnum {
      VERSION_1 = 'v1',
      VERSION_2 = 'v2',
    }
    
    class Discriminator {
      @IsEnum(VersionEnum)
      version: VersionEnum;
    }
    
    class A extends Discriminator {
      @IsDefined()
      @IsNumber()
      field: number;
    }
    
    class B extends Discriminator {
      @IsDefined()
      @IsString()
      field: string;
    }
    
    export class C {
      @ValidateNested({ each: true })
      @Type(() => Discriminator, {
        discriminator: {
          property: 'version',
          subTypes: [
            { value: A, name: VersionEnum.VERSION_1 },
            { value: B, name: VersionEnum.VERSION_2 },
          ],
        },
        keepDiscriminatorProperty: true,
      })
      array: (A | B)[];
    }
    

    Now your controller should look like this:

    @Controller()
    export class AppController {
      @Post()
      public example(
        @Body(new ValidationPipe({ transform: true }))
        body: C,
      ) {
        return body;
      }
    }
    

    And you can do your requests:

    curl --location 'localhost:3000/' \
    --header 'Content-Type: application/json' \
    --data '{
      "array": [
        {
          "version": "v1",
          "field": 1
        },
        {
          "version": "v2",
          "field": "2"
        }
      ]
    }'
    

    If you change the second element of the nested array to be a number with the version property as "v2", then you will see an error:

    {
      "version": "v2",
    - "field": "2"
    + "field": 2
    }
    
    {
      "statusCode": 400,
      "message": [
        "array.1.field must be a string"
      ],
      "error": "Bad Request"
    }
    

    References: https://github.com/typestack/class-transformer#providing-more-than-one-type-option