class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

question: How to compare that 1 of 2 fields is set

Open Chowarmaan opened this issue 3 years ago • 4 comments

I have two fields in my input that I want one of them to be set. In my use case, I have a user that we can look up by the Id, or Key.

@IsString() @MinLength(1) public readonly Id: string;
@IsNumber() @Min(1) public readonly Key: number;
@IsString() @MinLength(4) public readonly Name: string

While I can validate each field, I want to validate that one of the two (Id or Key) is valid. I would like field that is set, to still have to pass the normal validation (IsNumber, Min, etc.) that might be set on the field.

Optional allows both to be omitted. Groups does not help. Ideally I would like it to be exclusively one field or the other, and not both, but I could work with both being populated with valid values.

I would like to have something similar to oneOf, but instead of values, be able to specify this across the fields to validate.

Chowarmaan avatar Mar 24 '22 18:03 Chowarmaan

Have you looked into ValidateIf?

example:

export class Person {
  @IsString()
  @IsOptional()
  name?: string;

  @ValidateIf((person: Person) => Boolean(person.name))
  @IsString()
  @MinLength(4)
  @MaxLength(8)
  @IsOptional()
  nickname?: number;
}

if name is defined (Boolean(person.name) returns true), nickname will be checked to see if it's valid. If name is not defined, nickname's validation decorators will be skipped, so it passes validation no matter what its value is.

That way you can implement conditional logic that depends on other fields. You should be able to achieve what you want by using ValidateIf on both fields and setting up the right condition, but I acknowledge that it is a bit heavy handed.

If this issue is resolved, please comment your solution and close it :)

braaar avatar Aug 16 '22 07:08 braaar

Thanks. Let me go back to where I was using this and try this out. I want all the validation on either field to work, but I guess in your suggestion, the nickname is then required, and you only validate it if the name failed, and I guess the name might be the same? I need one of the 2 fields to be present, so the validation requires the @Required() on both, and then use the @ValidateIf() to not run the validation if one of the other pass. I need to check lengths etc., but I will give this a try and post back.

Chowarmaan avatar Aug 16 '22 13:08 Chowarmaan

I hope it works!

I'm not sure if you can somehow validate against the decorators of name inside the ValidateIf function. That would be sweet. I suppose it should be possible, since you have a Person object to work with. I'm sure there's a way.

braaar avatar Aug 16 '22 14:08 braaar

I was able to do a simple test in my code, and this will work to allow me to populate one option or another. I have added the simple test function and code below, to illustrate it working with a number or string parameter.

date.dto.ts

import { IsDefined, IsNumber, Min, Max, IsOptional, IsString, Length, ValidateIf } from '@nestjs/class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class DateDTO {
	@ApiProperty({
		description: 'Date as a number in the format YYYYMMDD',
		minimum: 20100101,
		maximum: 20991231
	})
	@IsOptional() @IsNumber() @Min(20100101) @Max(20991231) public readonly AsNumber?: number; // YYYYMMDD

	@ApiProperty({
		description: 'Date as a string in the format YYYY-MM-DD',
		maxLength: 10,
		minLength: 10
	})
	@ValidateIf( (DateDTOObj: DateDTO) => Boolean(! DateDTOObj.AsNumber))
		@IsDefined() @IsString() @Length(10) public readonly DateStr: string; // YYYY-MM-DD

	constructor(DateValue: number | string) {
		if (typeof DateValue === 'number') {
			this.AsNumber = DateValue;
		} else {
			this.DateStr = DateValue;
		}
	}
}

date.dto.spec.ts

import { validate, ValidationError } from '@nestjs/class-validator';
import { DateDTO } from './date.dto';

describe('DateDTO', () => {
	it('should be defined', () => {
		expect(new DateDTO(20220815)).toBeDefined();
	});

	it('should not generate the DTO errors with a number', async () => {
		const TestDTO: DateDTO = new DateDTO(20220815);
		const Errors: Array<ValidationError> = await validate(TestDTO);
		expect(Errors.length).toBe(0);
	});

	it('should except the string format of the date YYYY-MM-DD', () => {
		expect(new DateDTO('2022-08-15')).toBeDefined();
	});

	it('should not generate the DTO errors with a string', async () => {
		const TestDTO: DateDTO = new DateDTO('2022-08-15');
		const Errors: Array<ValidationError> = await validate(TestDTO);
		expect(Errors.length).toBe(0);
	});

	it('should generate the DTO errors when the numeric date is invalid', async () => {
		const TestDTO: DateDTO = new DateDTO(12345);
		const Errors: Array<ValidationError> = await validate(TestDTO);
		expect(Errors.length).toBe(1);
		expect(Errors[0].constraints['min']).toBe('AsNumber must not be less than 20000101');
	});

	it('should generate the DTO errors when there is not a numeric date', async () => {
		const TestDTO: DateDTO = new DateDTO('baddate');
		const Errors: Array<ValidationError> = await validate(TestDTO);
		expect(Errors.length).toBe(1);
		expect((Errors[0].constraints['isLength'])).toBe('DateStr must be longer than or equal to 10 characters');
	});
});

Chowarmaan avatar Sep 13 '22 15:09 Chowarmaan

Closing this as solved.

If the issue still persists, you may open a new Q&A in the discussions tab and someone from the community may be able to help.

NoNameProvided avatar Nov 13 '22 14:11 NoNameProvided

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Dec 14 '22 00:12 github-actions[bot]