DEV Community

Cover image for Validating a polymorphic body in nest JS
Julien Prugne for Webeleon

Posted on

Validating a polymorphic body in nest JS

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.
party

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'
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

wonderful

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';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And that's it!
Now you know!

Questions?

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)

Collapse
 
dexfs profile image
André Santos

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.