Validation does not work with Partial<DTO> - NestJS
Asked Answered
V

4

15

I want to apply server-side validation on my CRUD API. The entity in question is called Employee. I am using an employee.dto (shown below) for the create and update endpoints.

The class-validator package works fine on the create method but ignores all rules in the DTO when I use it with Partial<EmployeeDTO> in the update method.

Please use the code below for reference.

Packages

"class-transformer": "^0.2.3",
"class-validator": "^0.10.0",

Employee DTO

import { IsString, IsNotEmpty, IsEmail, IsEnum } from 'class-validator';

import { EmployeeRoles } from '../../entities/employee.entity';

export class EmployeeDTO {
  @IsString()
  @IsEmail()
  @IsNotEmpty()
  email: string;

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

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

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

  @IsString()
  @IsNotEmpty()
  @IsEnum(EmployeeRoles)
  role: string;
}

Employee Controller

import {
  Controller,
  Param,
  Post,
  Body,
  Put,
  UsePipes,
} from '@nestjs/common';

import { EmployeeDTO } from './dto/employee.dto';
import { EmployeeService } from './employee.service';
import { ValidationPipe } from '../shared/pipes/validation.pipe';

@Controller('employee')
export class EmployeeController {
  constructor(private employeeService: EmployeeService) {}

  @Post()
  @UsePipes(ValidationPipe)
  addNewEmployee(@Body() data: EmployeeDTO) {
    return this.employeeService.create(data);
  }

  @Put(':id')
  @UsePipes(ValidationPipe)
  updateEmployee(@Param('id') id: number, @Body() data: Partial<EmployeeDTO>) {
    return this.employeeService.update(id, data);
  }
}

Possible Solution

I work around I can think of is creating separate DTOs for create and update methods, but I don't like the idea of repeating the code.

Vinic answered 5/9, 2019 at 1:34 Comment(0)
O
12

For this answer, I'll take a guess and assume that you use the ValidationPipe provided in the NestJS' documentation, or a close derivative.

Your updateEmployee method's argument data type is Partial, which doesn't emit any type metadata. for the ValidationPipe to instantiate it using the class-transformer module, resulting in the class-validator module to validate a plain object, and not an EmployeeDTO.

For the validation to work, the type of the data argument should be a class. You could either make separate DTOs to create and update your entity, or use validation groups if you want to keep a single class.

Ovule answered 5/9, 2019 at 7:42 Comment(6)
Thank you for your answer! I plan to integrate Swagger in the future as well. Which approach would be better, having separate DTOs or keep a single class?Vinic
and Yes, I am using Validation PipeVinic
For Swagger, I think having separate DTOs would make for a clearer documentation.Ovule
Yes, I had the same thought, although it repeats a lot of the code. Thanks.Vinic
Even though it duplicates some code, IMO it allows you to have better comprehension and separation of concerns. Also I assume that when you create an employee, some attributes of your DTO class are required, while when you update it, they're optional.Ambulator
do seprate DTO is good approach but about if my dto is large and i dont want to write again this DTO for update please see my issue - #78238731Pylos
U
32

In order to achieve partial validation, you can use PartialType utility function. You can read about it here: https://docs.nestjs.com/openapi/mapped-types#partial

You would need to create another class:

export class UpdateEmployeeDTO extends PartialType(EmployeeDTO) {}

and then in your controller, you need to replace the type of @Body data Partial<EmployeeDTO> to UpdateEmployeeDto. It should look like this:

@Patch(':id')
@UsePipes(ValidationPipe)
updateEmployee(@Param('id') id: number, @Body() data: UpdateEmployeeDTO) {
    return this.employeeService.update(id, data);
}

Please keep in mind that you should import PartialType from @nestjs/mapped-types not from @nestjs/swagger like suggested in the documentation. More about this can be found here

Urata answered 16/1, 2021 at 10:25 Comment(7)
Could you please add to your answer that PartialType needs to be imported from @nestjs/mapped-types? The documentation was misleading. Also the updateEmployee function should be PATCH not PUT. PUT is meant to replace everything and PATCH to update only specific values.Arv
@Arv thanks for the suggestions. I have left Put because it was originally used in the question, but you are right - we should use Patch because PartialType allows to edit only a part of the requested object not necessarily whole object. I have changed that, thanks. I wonder if @nestjs/mapped-types is a correct import. Why do you think the documentation was misleading? I have wrote few sample unit tests and verified that PartialType from @nestjs/swagger package (which is suggested in the documentation) is working as expected as well.Urata
It does not work with @nestjs/swagger, only if you actually use swagger. Otherwise it should be @nestjs/mapped-types. It is not documented but there is an open issue to documentate that: github.com/nestjs/docs.nestjs.com/issues/1795Arv
Thanks @Arv I didn't know about that. I have put information about this in my post.Urata
It is now documented here: docs.nestjs.com/techniques/validation#mapped-types -> There is even a third library @nestjs/graphql which needs to be used when using graphql.Arv
Seems that @nestjs/swagger is fine now except when in combination with @ValidateNested. With @nestjs/mapped-types all is fine.Incense
i have the same issue about the PartialType i'm trying import from both @nest/swagger and @nestjs/mapped-types here is problem url- #78238731Pylos
O
12

For this answer, I'll take a guess and assume that you use the ValidationPipe provided in the NestJS' documentation, or a close derivative.

Your updateEmployee method's argument data type is Partial, which doesn't emit any type metadata. for the ValidationPipe to instantiate it using the class-transformer module, resulting in the class-validator module to validate a plain object, and not an EmployeeDTO.

For the validation to work, the type of the data argument should be a class. You could either make separate DTOs to create and update your entity, or use validation groups if you want to keep a single class.

Ovule answered 5/9, 2019 at 7:42 Comment(6)
Thank you for your answer! I plan to integrate Swagger in the future as well. Which approach would be better, having separate DTOs or keep a single class?Vinic
and Yes, I am using Validation PipeVinic
For Swagger, I think having separate DTOs would make for a clearer documentation.Ovule
Yes, I had the same thought, although it repeats a lot of the code. Thanks.Vinic
Even though it duplicates some code, IMO it allows you to have better comprehension and separation of concerns. Also I assume that when you create an employee, some attributes of your DTO class are required, while when you update it, they're optional.Ambulator
do seprate DTO is good approach but about if my dto is large and i dont want to write again this DTO for update please see my issue - #78238731Pylos
A
6

I ran into this exact issue and would like to share some discoveries as well as a solution.

Let's pretend we have this class:

User {
  @IsNotEmpty()
  name: string;
  @IsOptional()
  nickname?: string;
}

On POST you want all validations to apply. On PATCH (or PUT) you want only validations to apply for properties that are specified. Well, POST is no problem, in the controller do:

@Body() params: User

But, PATCH (or PUT) is problematic because there is no way to handle partial validations out of the box without applying it universally.

Here are some solutions I explored:

Solution 1: PartialType [ FAIL ]

import { OmitType, PartialType } from '@nestjs/swagger';

UpdateUser extends PartialType(User) {}

Then, in the controller:

@Body() params: UpdateUser,

Using NestJS' PartialType does not override the class validator annotations on the parent class. So, if you have a @IsNotEmpty() on the parent class it will still make it required.

Solution 2: Partial [ FAIL ]

In the controller:

@Body() params: Partial<User>

None of the class validator annotations apply. This is because generics are not supported, as explained elsewhere.

Solution 3: Validation within controller [ PASS, not recommended ]

In the controller:

@Body() params: any

Then, in the controller method call class validator's validate() directly with something like:

const transformedValue = plainToClassFromExist(new User(), params);
const errors = validate(transformedValue, {
  skipUndefinedProperties: true
});

The magic here is skipUndefinedProperties. If the property does not exist (ie is undefined) then class validator will not validate it. Note that @IsDefined() bypasses this.

This would work, but will result in lots of duplicate code.

Solution 4: Use a custom decorator [ PASS, RECOMMENDED ]

Create a decorator called PartialBody that reads the type information from an enhancer and then runs validations, see https://gist.github.com/josephdpurcell/d4eff886786d58f58b86107c0947e19e as an example.

Make sure validateCustomDecorators=false in your global validation pipe.

Now, there are a few variations of how to use it:

Variation 1: Pass the type [ PASS, RECOMMENDED ]

In the controller:

@PartialBody(User) params: Partial<User>

When PartialBody runs it will retrieve the type from the passed argument. This will only validate properties that exist, and will ensure params is a partial of User. Huzzah!

Variation 2: Use partial [ FAIL ]

In the controller:

@PartialBody() params: Partial<User>

This fails because generics aren't supported. When the PartialBody decorator logic is run it has access to no type information.

Variation 3: Use class [ FAIL ]

In the controller:

@PartialBody() params: User

While this would perform partial validations just like Variation 1, it fails because params would be seen as a full type instead of a partial type. While writing code your editor would think that all properties are there when they may not be.

Variation 4: Extend PartialType [ PASS ]

Create a new type with PartialType:

import { PartialType } from '@nestjs/swagger';

export class PartialUser extends PartialType(User) {}

In the controller:

@PartialBody() params: PartialUser

This works just like Variation 1, but you have to create an additional class. This may be desired if you wish to override class validator checks by overwriting the properties and redeclaring decorators.

Antimonyl answered 7/9, 2022 at 15:48 Comment(4)
Where can the @PartialBody be found? Nest.js doesn't have any entry in the documentation.Baelbeer
You answer is great. I've tried to apply on my code, but the problem is on the Swagger we do not have the required body section.Krick
@Baelbeer Unfortunately I didn't see your comment until now, I've updated the answer to clarify where PartialBody is found, it's here: gist.github.com/josephdpurcell/d4eff886786d58f58b86107c0947e19eAntimonyl
@CarlosQuerioz thanks! I didn't explore swagger compatibility with these solutions. If there is an improvement I'm happy to edit the answer.Antimonyl
L
1

This article opened my mind and gave me a better understanding: https://medium.com/fusionworks/api-payloads-validation-and-transformation-in-nestjs-5022ce4df225

It all started because I am lazy and don't want to write multiple classes (a create DTO and an update DTO). Plus, you make one change in one class, you have to remember make the change in the other one.

As a result, my updateDTO extends my createDTO & my initial approach was with PartialType imported from @nestjs/mapped-types.

Given the following example:

export class CreateEmployeeDTO {
    // might wanna do this if you want to take an updateDto and transform to an createDto
    @Exclude()
  id?: number;
  
  @IsString()
  @IsNotEmpty()
  username: string;
  
  @IsSring()
  @IsNotEmpty()
  password: string;
  
  @IsString()
  @IsNotEmpty()
  role: string;
}

For the UpdateEmployeeDTO I extend it using OmitType instead of PartialType, because I am assuming when I am updating the Employee:

  • I need to pass an id of the employee
  • I might not wanna change the password, make it optional
  • I want to have the client pass the username & role all the time

Therefore, my UpdateEmployeeDTO looks as such:

export class UpdateEmployeeDTO extends OmitType(CreateEmployeeDTO, ['id', 'password']) {
    @IsNumber()
  @IsNotEmpty()
  id: number;
  
  @IsString()
  @IsOptional()
  password: string;
}

The second array passed to the OmitType are the properties omitted from CreateEmployeeDTO.

Therefore, on update if I update the employee and not give it an username or role, my ValidationPipe will produce the appropriate error according to class-validator.

As a result, with the respect to the question, all rules in the (create) DTO will not be ignored.

Read the article above, very well written.

Lazy answered 24/1, 2023 at 17:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.