class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

ValidateNested on Array of objects fail with "property should not exists"

Open smasilamani-cfins opened this issue 2 years ago • 8 comments

We are running our application in NestJS framework and we are getting an error when we do the validation on the array of objects. Especially when using ValidateNested annotation. on Array of objects that fail with "property should not exists"

From our package.json "class-transformer": "0.5.1", "class-validator": "^0.14.0",

Node : v18

export class PolDumpReq {
	@Type(() => PolDump)
	@IsArray({ always: true })
	@ArrayMinSize(1, { always: true })
	@IsNotEmpty({ each: true, always: true })
	@ValidateNested({ always: true, each: true })
	polDumps: PolDump[];
}

export class PolDump {
	@IsInt({ always: true })
	@IsNotEmpty({ always: true })
	versionNum: number;

	@IsString({ always: true })
	@IsNotEmpty({ always: true })
	polNumber: string;

	@IsString({ always: true })
	@IsNotEmpty({ always: true })
	@Matches(Constants.DATE_FORMAT_REGEXP, { always: true, message: `effectiveDate. Use ISO format ${Constants.DATE_FORMAT}.` })
	effectiveDate: string;

	@IsString({ always: true })
	@IsNotEmpty({ always: true })
	@Matches(Constants.DATE_FORMAT_REGEXP, { always: true, message: `effectiveDate. Use ISO format ${Constants.DATE_FORMAT}.` })
	expirationDate: string;

	@Type(() => PolVehicleDump)
	@IsArray({ always: true })
	@ArrayMinSize(0, { always: true })
	@IsOptional({ always: true })
	@ValidateNested({ always: true })
	polVehicleDumps: PolVehicleDump[];
}

export class PolVehicleDump {
	@IsString()
	@IsNotEmpty()
	vin: string;

	@IsInt()
	@IsOptional()
	year: number;

	@IsString()
	@IsOptional()
	make: string;

	@IsString()
	@IsOptional()
	model: string;

	@IsInt()
	@IsOptional()
	vehicleTypeCode: number;
}

Request:

{
    "polDumps": [
        {
            "polNumber": "123123",
            "versionNum": 1,
            "effectiveDate": "2020-12-10",
            "expirationDate": "2021-12-10",
            "polVehicleDumps": [
                {
                    "vin": "XXXXXXX",
                    "year": 2005,
                    "make": "FREIGHTLINER",
                    "model": "CL120T",
                    "vehicleTypeCode": 7,
                }
            ]
        }
    ]
}

Response:

"polDumps.0.polVehicleDumps.0.property vin should not exist",
"polDumps.0.polVehicleDumps.0.property year should not exist",
"polDumps.0.polVehicleDumps.0.property make should not exist",
"polDumps.0.polVehicleDumps.0.property model should not exist",
"polDumps.0.polVehicleDumps.0.property vehicleTypeCode should not exist"

Expected behavior

Expected to pass the validation

Actual behavior

Nested objects fail the validation.

smasilamani-cfins avatar May 12 '23 17:05 smasilamani-cfins

Cannot reproduce your problem, here is how I test it (you didn't provide the regex const, so I skipped the regex match decorator):

/* eslint-disable no-process-exit */
import 'reflect-metadata';
import { Type, plainToInstance } from 'class-transformer';
import {
  ArrayMinSize,
  IsArray,
  IsInt,
  IsNotEmpty,
  IsOptional,
  IsString,
  ValidateNested,
  validate,
} from 'class-validator';

export class PolDumpReq {
  @Type(() => PolDump)
  @IsArray({ always: true })
  @ArrayMinSize(1, { always: true })
  @IsNotEmpty({ each: true, always: true })
  @ValidateNested({ always: true, each: true })
  polDumps: PolDump[];
}

export class PolDump {
  @IsInt({ always: true })
  @IsNotEmpty({ always: true })
  versionNum: number;

  @IsString({ always: true })
  @IsNotEmpty({ always: true })
  polNumber: string;

  @IsString({ always: true })
  @IsNotEmpty({ always: true })
  effectiveDate: string;

  @IsString({ always: true })
  @IsNotEmpty({ always: true })
  expirationDate: string;

  @Type(() => PolVehicleDump)
  @IsArray({ always: true })
  @ArrayMinSize(0, { always: true })
  @IsOptional({ always: true })
  @ValidateNested({ always: true })
  polVehicleDumps: PolVehicleDump[];
}

export class PolVehicleDump {
  @IsString()
  @IsNotEmpty()
  vin: string;

  @IsInt()
  @IsOptional()
  year: number;

  @IsString()
  @IsOptional()
  make: string;

  @IsString()
  @IsOptional()
  model: string;

  @IsInt()
  @IsOptional()
  vehicleTypeCode: number;
}

(async () => {
  const req = plainToInstance(PolDumpReq, {
    polDumps: [
      {
        polNumber: '123123',
        versionNum: 1,
        effectiveDate: '2020-12-10',
        expirationDate: '2021-12-10',
        polVehicleDumps: [
          {
            vin: 'XXXXXXX',
            year: 2005,
            make: 'FREIGHTLINER',
            model: 'CL120T',
            vehicleTypeCode: 7,
          },
        ],
      },
    ],
  });
  console.log(req);
  // PolDumpReq {
  //   polDumps: [
  //     PolDump {
  //       polNumber: '123123',
  //       versionNum: 1,
  //       effectiveDate: '2020-12-10',
  //       expirationDate: '2021-12-10',
  //       polVehicleDumps: [Array]
  //     }
  //   ]
  // }
  console.log(await validate(req)); // []
})()
  .then(() => {
    process.exit(0);
  })
  .catch((error) => {
    console.log(error);
    process.exit(1);
  });

kpkonghk01 avatar Jun 21 '23 04:06 kpkonghk01

I am having the same problem, although I'm just a beginner here and it might be something I did wrong. The issue happens when I have a Dto referencing another, and the format of the request is multipart/form-data like:

//create-entity-one.dto.ts
export class CreateEntityOneDto {
  @Transform(({ value }) => {
    try {
      return JSON.parse(value);
    } catch {
      throw new Error("Invalid JSON format for entityTwo.");
    }
  })
  @Type(() => CreateEntityTwoDto)
  @IsOptional()
  @ValidateNested()
  @ApiProperty({ type: CreateEntityTwoDto})
  entityTwo: CreateEntityTwoDto;
}
//entity-one.controller.ts
  @Post()
  @ApiConsumes("multipart/form-data")
  createInstalacao(
    @Body() body: CreateEntityOneDto,
  ) {
    return this.entitiesOneService.create(body, files);
  }

When trying to post, it takes all the properties of CreateEntityTwoDto and says it should not exist. It does not happen when you have @IsOptional(), but happens again if you have @IsOptional and @ValidateNested().

joaolabofcodes avatar Feb 29 '24 13:02 joaolabofcodes

I have the same problem

mokulus avatar Apr 01 '24 17:04 mokulus

@joaolabofcodes / @mokulus

In my case, all of our models are in a shared project called lib and when I moved the models to the server side project, then it started working but that means, we are duplicating our models across the projects. The reason why we have lib is because we want to share some of our models between client and server side.

Our project structure:

client lib server

smasilamani-cfins avatar Apr 22 '24 17:04 smasilamani-cfins

In my case I was using ValidateNested in DTOs for a NestJs API and the child properties were not being transformed correctly by NestJs's ValidationPipe or Type decorator, so I created a custom PipeTransform which uses class-transformer's plainToClass to ensure the data being validated has been instantiated as the correct class.

Due to some more weirdness with ValidationPipe, I had to call my custom pipe, then ValidationPipe (with transform: false), then my custom pipe again. After this, class-validator works as expected.

Long story short, if you're having this issue I recommend making sure the data being passed to class-transformer is actually an instance of the class you want, and all nested properties are the correct instances as well.

benvogler avatar May 21 '24 16:05 benvogler