Why Can't `IsDecimal` Validate Numbers?
In NestJS, class-validator is a powerful tool that simplifies data validation. By adding a few decorators to a DTO (Data Transfer Object), you can easily control the structure of incoming requests. However, sometimes decorators behave differently than expected, causing confusion. One such decorator is @IsDecimal.
Have you ever encountered an error saying “not a valid decimal” even though you sent a properly formatted decimal number? In this post, I’ll clearly explain why @IsDecimal behaves this way and how to properly validate decimal numbers in NestJS.
The Misunderstanding About @IsDecimal
Many developers try to use @IsDecimal to validate decimal values of the number type. However, as seen in various discussions on Stack Overflow, @IsDecimal only checks whether a string value matches a decimal format.
In fact, the GitHub issue (#1423) for class-validator documents that its description comment was misleading in the past, causing confusion. While this has since been corrected, many developers still get tripped up by it.
Take a look at the following code:
// create-product.dto.ts
import { IsDecimal, IsNotEmpty, Min } from 'class-validator';
export class CreateProductDto {
@IsDecimal({ decimal_digits: '2' }) // 🚨 This is where the issue occurs!
@IsNotEmpty()
@Min(0)
price: number; // Type is 'number'.
}
If you send a request like { "price": 1574.23 } in JSON format, class-validator will throw an error because the price field is of number type, and thus does not pass the @IsDecimal check.
So how do we validate decimal numbers with number type? There are two solutions.
Solution 1: Use @IsNumber
The simplest and most straightforward solution is to use the @IsNumber decorator. It validates values of type number and allows you to set the maximum number of decimal places using the maxDecimalPlaces option.
// create-product.dto.ts (after modification)
import { IsNumber, IsNotEmpty, Min } from 'class-validator';
export class CreateProductDto {
@IsNumber({ maxDecimalPlaces: 2 }) // ✅ Modify it like this!
@IsNotEmpty()
@Min(0)
price: number;
}
With this change, the price field will be properly validated as a number with up to two decimal places allowed.
Tip: To ensure database precision, if you’re using an ORM like TypeORM, it’s a good idea to also define the column with the @Column('decimal') decorator in your entity file.
Solution 2: Create a Custom Decorator
If you need more complex validation rules, you can create a custom decorator. Using a library like exonerate, you can combine multiple rules to build powerful custom decorators easily.
First, install the library:
npm install exonerate
Then you can use the @Exonerate decorator in your DTO to apply various rules like this:
// create-user.dto.ts (Example using exonerate)
import { Exonerate } from 'exonerate';
import { User } from './user.entity'; // Assume there is a User entity
import { AddressDto } from './address.dto'; // Assume there is an Address DTO
enum ROLE {
ADMIN = 'ADMIN',
USER = 'USER',
}
export class CreateUserDto {@Exonerate({ rules: 'required|string|max:20|min:4|exist:name', entity: User })name: string;
@Exonerate({ rules: 'required|email|unique:email', entity: User })email: string;
@Exonerate({ rules: 'required|max:20|min:8|pattern', regexPattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/})password: string;
@Exonerate({ rules: 'required|enum', enumType: ROLE })role: string;
}
exonerate helps you express complex validation rules succinctly—not just decimal checks but also things like email uniqueness (unique:email) and pattern matching (pattern). It’s very useful when you need consistent and reusable validation logic across your project.
Set Up a Global Pipe
To make class-validator work across your application, you need to add a ValidationPipe to the main.ts file. Don’t forget this setup!
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// ✅ Apply global validation pipe.
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
Also, if you’re using environment variables, it’s common to configure ConfigModule in your app.module.ts.
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env',}),// ... other modules
],
controllers: [],
providers: [],
})
export class AppModule {}
Use the Right Tool for the Right Job
Through this journey, we’ve learned a valuable lesson: @IsDecimal is intended for strings, and you should use @IsNumber to validate decimal values of number type.
Taking time to carefully read documentation for decorators and functions can protect you from unexpected bugs. If official docs are lacking or unclear, don’t hesitate to raise questions on GitHub issues or community forums to get clarity!