乐闻世界logo
搜索文章和话题

How to use validators in TypeORM? Including class-validator integration and custom validator implementation

2月18日 19:12

Data validation is an important aspect of application development. TypeORM can integrate with various validator libraries to ensure data integrity and consistency. This article details how to use validators in TypeORM for data validation.

Validator Basic Concepts

What are Validators

Validators are mechanisms for verifying that data meets specific rules, including:

  • Field type validation
  • Field format validation
  • Field length validation
  • Custom business rule validation
  • Cross-field validation

Common Validator Libraries

  • class-validator: Most popular TypeScript validator library
  • class-transformer: For object transformation and validation
  • joi: Powerful object schema validation library
  • zod: TypeScript-first schema validation library

Using class-validator

Installing Dependencies

bash
npm install class-validator class-transformer npm install --save-dev @types/class-transformer

Basic Validation Example

typescript
import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert, BeforeUpdate } from 'typeorm'; import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsInt, Min, Max, IsOptional, IsDateString, IsEnum, ValidateIf, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty({ message: 'Name cannot be empty' }) @IsString({ message: 'Name must be a string' }) @MinLength(2, { message: 'Name must be at least 2 characters' }) @MaxLength(100, { message: 'Name must not exceed 100 characters' }) name: string; @Column({ unique: true }) @IsEmail({}, { message: 'Invalid email format' }) @IsNotEmpty({ message: 'Email cannot be empty' }) email: string; @Column({ nullable: true }) @IsOptional() @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(100, { message: 'Password must not exceed 100 characters' }) password?: string; @Column({ type: 'int', nullable: true }) @IsOptional() @IsInt({ message: 'Age must be an integer' }) @Min(18, { message: 'Age must be at least 18' }) @Max(120, { message: 'Age must not exceed 120' }) age?: number; @Column({ type: 'enum', enum: ['active', 'inactive', 'suspended'], default: 'active' }) @IsEnum(['active', 'inactive', 'suspended'], { message: 'Invalid status' }) status: string; @Column({ type: 'date', nullable: true }) @IsOptional() @IsDateString({}, { message: 'Invalid date format' }) birthDate?: Date; @BeforeInsert() @BeforeUpdate() async validate() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Validation failed: ${JSON.stringify(errors)}`); } } }

Advanced Validation

Custom Validators

typescript
import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions } from 'class-validator'; // Custom validator: Check if username is unique @ValidatorConstraint({ name: 'isUsernameUnique', async: true }) export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface { async validate(username: string) { // Should query database to check if username is unique // Example code const userExists = await checkUsernameExists(username); return !userExists; } defaultMessage(args: ValidationArguments) { return 'Username already exists'; } } // Custom decorator export function IsUsernameUnique(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsUsernameUniqueConstraint, }); }; } // Use custom validator @Entity() export class User { @Column({ unique: true }) @IsUsernameUnique({ message: 'Username already exists' }) username: string; }

Conditional Validation

typescript
import { ValidateIf } from 'class-validator'; @Entity() export class User { @Column() @IsNotEmpty() accountType: 'personal' | 'business'; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Company name is required for business accounts' }) companyName?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'business') @IsNotEmpty({ message: 'Tax ID is required for business accounts' }) taxId?: string; @Column({ nullable: true }) @ValidateIf(o => o.accountType === 'personal') @IsNotEmpty({ message: 'Personal ID is required for personal accounts' }) personalId?: string; }

Nested Object Validation

typescript
import { ValidateNested, Type } from 'class-transformer'; import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; class Address { @IsNotEmpty() @IsString() street: string; @IsNotEmpty() @IsString() city: string; @IsNotEmpty() @IsString() zipCode: string; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() @IsNotEmpty() name: string; @Column({ type: 'json', nullable: true }) @ValidateIf(o => o.hasAddress) @ValidateNested() @Type(() => Address) address?: Address; @Column({ default: false }) hasAddress: boolean; }

Cross-Field Validation

typescript
import { ValidatorConstraint, ValidatorConstraintInterface, registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; // Custom validator: Confirm password matches @ValidatorConstraint({ name: 'isPasswordMatching', async: false }) export class IsPasswordMatchingConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments) { const object = args.object as any; return password === object.password; } defaultMessage(args: ValidationArguments) { return 'Passwords do not match'; } } export function IsPasswordMatching(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsPasswordMatchingConstraint, }); }; } @Entity() export class User { @Column() @IsNotEmpty() @MinLength(8) password: string; @Column({ nullable: true }) @IsPasswordMatching({ message: 'Passwords do not match' }) confirmPassword?: string; }

Validation Error Handling

Validate and Get Errors

typescript
import { validate, ValidationError } from 'class-validator'; async function createUser(userData: Partial<User>) { const user = new User(); Object.assign(user, userData); const errors = await validate(user); if (errors.length > 0) { // Format error messages const formattedErrors = this.formatValidationErrors(errors); throw new Error(`Validation failed: ${JSON.stringify(formattedErrors)}`); } // Save user return await userRepository.save(user); } function formatValidationErrors(errors: ValidationError[]): any { const result: any = {}; errors.forEach(error => { const constraints = error.constraints || {}; result[error.property] = Object.values(constraints).join(', '); if (error.children && error.children.length > 0) { result[error.property] = { ...result[error.property], ...this.formatValidationErrors(error.children) }; } }); return result; } // Usage example try { const user = await createUser({ name: '', email: 'invalid-email', age: 15 }); } catch (error) { console.error(error.message); // Output: Validation failed: {"name":"Name cannot be empty","email":"Invalid email format","age":"Age must be at least 18"} }

Custom Validation Middleware

typescript
import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; export function validationMiddleware<T extends object>( type: new () => T ) { return async (req: any, res: any, next: any) => { const dto = plainToClass(type, req.body); const errors = await validate(dto); if (errors.length > 0) { const formattedErrors = formatValidationErrors(errors); return res.status(400).json({ error: 'Validation failed', details: formattedErrors }); } req.body = dto; next(); }; } // Use in Express import express from 'express'; const app = express(); app.post('/users', validationMiddleware(User), async (req, res) => { const user = await userRepository.save(req.body); res.json(user); } );

Validator Decorators Explained

String Validation

typescript
@Entity() export class User { @Column() @IsString() @IsNotEmpty() @MinLength(2) @MaxLength(100) @IsAlphanumeric() name: string; @Column() @IsEmail() @IsLowercase() email: string; @Column() @IsUrl() website?: string; @Column() @IsPhoneNumber(null) // Need to install class-validator-phone-number phone?: string; }

Number Validation

typescript
@Entity() export class Product { @Column({ type: 'decimal', precision: 10, scale: 2 }) @IsNumber() @Min(0) @Max(999999.99) price: number; @Column({ type: 'int' }) @IsInt() @IsPositive() stock: number; @Column({ type: 'int' }) @IsInt() @IsDivisibleBy(10) quantity: number; }

Date Validation

typescript
@Entity() export class Event { @Column({ type: 'date' }) @IsDateString() @IsBefore('endDate') startDate: Date; @Column({ type: 'date' }) @IsDateString() @IsAfter('startDate') endDate: Date; @Column({ type: 'date' }) @IsDateString() @IsFuture() registrationDeadline?: Date; }

Array and Object Validation

typescript
@Entity() export class User { @Column({ type: 'simple-array' }) @IsArray() @ArrayNotEmpty() @ArrayMinSize(1) @ArrayMaxSize(10) @IsString({ each: true }) tags: string[]; @Column({ type: 'json', nullable: true }) @IsObject() @IsNotEmptyObject() metadata?: Record<string, any>; @Column({ type: 'simple-array', nullable: true }) @IsArray() @ArrayUnique() @IsEmail({ each: true }) additionalEmails?: string[]; }

Validation Best Practices

1. Layered Validation

typescript
// Entity layer validation: Database-level validation @Entity() export class User { @Column() @IsNotEmpty() @IsString() name: string; @BeforeInsert() @BeforeUpdate() async validateEntity() { const errors = await validate(this); if (errors.length > 0) { throw new Error(`Entity validation failed: ${JSON.stringify(errors)}`); } } } // DTO layer validation: API request-level validation class CreateUserDto { @IsNotEmpty() @IsString() @MinLength(2) @MaxLength(100) name: string; @IsNotEmpty() @IsEmail() email: string; @IsNotEmpty() @MinLength(8) password: string; } // Use DTO validation in service layer async function createUser(dto: CreateUserDto) { const errors = await validate(dto); if (errors.length > 0) { throw new ValidationException(errors); } const user = new User(); Object.assign(user, dto); return await userRepository.save(user); }

2. Async Validation

typescript
@ValidatorConstraint({ name: 'isEmailUnique', async: true }) export class IsEmailUniqueConstraint implements ValidatorConstraintInterface { async validate(email: string) { const user = await userRepository.findOne({ where: { email } }); return !user; } defaultMessage() { return 'Email already exists'; } } @Entity() export class User { @Column({ unique: true }) @IsEmailUnique() email: string; }

3. Internationalized Error Messages

typescript
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; @ValidatorConstraint({ name: 'customValidator', async: false }) export class CustomValidatorConstraint implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { return true; } defaultMessage(args: ValidationArguments) { // Return different error messages based on locale const locale = args.object['locale'] || 'en'; const messages = { en: 'Custom validation failed', zh: '自定义验证失败', ja: 'カスタム検証に失敗しました' }; return messages[locale] || messages.en; } }

4. Performance Optimization

typescript
// Avoid time-consuming operations in validators @ValidatorConstraint({ name: 'isUnique', async: true }) export class IsUniqueConstraint implements ValidatorConstraintInterface { private cache = new Map<string, boolean>(); async validate(value: any, args: ValidationArguments) { const cacheKey = `${args.targetName}.${args.property}.${value}`; // Check cache if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // Perform validation const result = await this.checkUniqueness(value, args); // Cache result this.cache.set(cacheKey, result); return result; } private async checkUniqueness(value: any, args: ValidationArguments): Promise<boolean> { // Actual uniqueness check logic return true; } }

5. Testing Validators

typescript
import { validate } from 'class-validator'; describe('User Validation', () => { it('should validate valid user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 25; const errors = await validate(user); expect(errors.length).toBe(0); }); it('should fail validation for invalid email', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'invalid-email'; user.age = 25; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('isEmail'); }); it('should fail validation for underage user', async () => { const user = new User(); user.name = 'John Doe'; user.email = 'john@example.com'; user.age = 15; const errors = await validate(user); expect(errors.length).toBeGreaterThan(0); expect(errors[0].constraints).toHaveProperty('min'); }); });

Integration with Other Libraries

Integration with Joi

typescript
import * as Joi from 'joi'; const userSchema = Joi.object({ name: Joi.string().min(2).max(100).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(18).max(120).optional(), password: Joi.string().min(8).required() }); @Entity() export class User { @BeforeInsert() @BeforeUpdate() async validateWithJoi() { const { error } = userSchema.validate(this); if (error) { throw new Error(`Validation failed: ${error.details[0].message}`); } } }

Integration with Zod

typescript
import { z } from 'zod'; const userSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(18).max(120).optional(), password: z.string().min(8) }); @Entity() export class User { @BeforeInsert() @BeforeUpdate() async validateWithZod() { const result = userSchema.safeParse(this); if (!result.success) { throw new Error(`Validation failed: ${JSON.stringify(result.error.errors)}`); } } }

TypeORM's validator functionality provides powerful data validation capabilities. Proper use of validators can ensure data integrity and consistency, improving application robustness.

标签:TypeORMClass Validator