Sometimes, an issue rise, this time I had to validate a body that could be of two distinct forms.
I could have chosen to build a big dto mixing both classes validation.
But in the end, it was kind of ugly, lacking the inherent elegance of Nest.
Today, I'll share with you my solution and the reasons for its necessity.
Here is our target controller method signature:
import { Controller, Post } from '@nestjs/common';
import { CollegeStudentDto, OnlineStudentDto } from './student.dto';
@Controller('student')
export class StudentController {
@Post()
signup(signupDto: CollegeStudentDto | OnlineStudentDto) {
return 'call the service and apply some logic'
}
}
Looks nice, eh?
Unfortunately, it won't work. The reflected metadata used in the ValidationPipe only knows how to cast to one class.
It can't discriminate the data and guess which of the classes to use for validation.
Ok, first thing first, let's define the DTOs:
import { IsNotEmpty, IsString } from 'class-validator';
export enum StudentType {
ONLINE = 'online',
COLLEGE = 'college',
}
export class StudentDto {
@IsString()
@IsNotEmpty()
firstName: string;
@IsString()
@IsNotEmpty()
lastName: string;
}
export class CollegeStudentDto extends StudentDto {
@IsString()
@IsNotEmpty()
college: string;
}
export class OnlineStudentDto extends StudentDto {
@IsString()
@IsNotEmpty()
platform: string;
}
So, how can we compensate for these limitations?
Easy! use setup our own transform pipe in the @Body()
annotation
import {
BadRequestException,
Body,
Controller,
Post,
ValidationPipe,
} from '@nestjs/common';
import { CollegeStudentDto, OnlineStudentDto } from './student.dto';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Controller('student')
export class StudentController {
@Post()
signup(
@Body({
transform: async (value) => {
let transformed: CollegeStudentDto | OnlineStudentDto;
if (value.college) {
// use plainToClass with older class-transformer versions
transformed = plainToInstance(CollegeStudentDto, value);
} else if (value.platform) {
transformed = plainToInstance(OnlineStudentDto, value);
} else {
throw new BadRequestException('Invalid student signup');
}
const validation = await validate(transformed);
if (validation.length > 0) {
const validationPipe = new ValidationPipe();
const exceptionFactory = validationPipe.createExceptionFactory();
throw exceptionFactory(validation);
}
return transformed;
},
})
signupDto: CollegeStudentDto | OnlineStudentDto,
) {
if (signupDto instanceof CollegeStudentDto) {
return 'college student';
} else if (signupDto instanceof OnlineStudentDto) {
return 'online student';
}
}
}
And that's it!
Now you know!
Questions?
I'll be glad to answers questions in the comments.
If you liked my discord consider joining my coding lair!
☎️Webeleon coding lair on discord
You can also email me and offer me a contract 💰
✉️Email me!
And since I'm a nice guy, here, take this sample repo containing a working codebase!
🎁Get the code of the tuto from github
Top comments (1)
Awesome!! I was looking for something this, I found a solution on stackoverflow before found yours, but I had the same idea and improved my solution after read your article.
I create a custom ValidationPipe and use the @UsePipes decorator. It was the difference.