24

e.g. for database rows, we may need nullable properties that must not be undefined:

class DbRow {
  @IsNumber()
  id!: number;

  @IsNumber()
  numNullable!: number | null;
}

So numNullable can be a number or null - but it must never be undefined.

How can we express this in class-validator?

  • adding @Optional() does not work, because that would also allow undefined
  • I also had no luck with a custom validator
TmTron
  • 17,012
  • 10
  • 94
  • 142

4 Answers4

47

It turns out that this is possible by using conditional validation ValidateIf:

class DbRow {
  @IsNumber()
  id!: number;

  @IsNumber()
  @ValidateIf((object, value) => value !== null)
  numNullable!: number | null;
}

Here is a stackblitz example

TmTron
  • 17,012
  • 10
  • 94
  • 142
  • Thanks, this worked for me. What is weird though is that the condition has to return false in order for it to ignore other validators, but I feel like it would be more intuitive if it was the other way around: Validate using the other validators IF I return true. But oh well. – Yuniac Jan 18 '23 at 09:48
10

Here is my solution:

import { ValidationOptions, ValidateIf } from 'class-validator';
export function IsNullable(validationOptions?: ValidationOptions) {
  return ValidateIf((_object, value) => value !== null, validationOptions);
}

Usage

import { plainToClass } from 'class-transformer';
import { IsNumber, validateSync } from 'class-validator';
import { IsNullable } from 'src/common/utils/is-nullable.decorator';
class SampleDto {
  @IsNullable()
  @IsNumber()
  foo: number | null;
}
describe('IsNullable', () => {
  it('should disable other validators when given property is null', () => {
    expect(validateSync(plainToClass(SampleDto, { foo: null }))).toEqual([]);
  });
  it('should allow other validators to work when given property is not null', () => {
    expect(validateSync(plainToClass(SampleDto, { foo: 1 }))).toEqual([]);
    expect(validateSync(plainToClass(SampleDto, { foo: '1' }))[0].constraints.isNumber).toMatch('foo must be a number');
  });
  it('should not allow undefined', () => {
    expect(validateSync(plainToClass(SampleDto, { foo: undefined })).length).toBeGreaterThan(0);
  });
});
glinda93
  • 7,659
  • 5
  • 40
  • 78
2

This is an extended version of IsOptional exported from class-validator.

import {
  ValidationOptions,
  ValidateIf,
  IsOptional as IsOptionalValidator,
} from 'class-validator';

/**
 * Checks if value is missing and if so, ignores all validators.
 *
 * @param nullable If `true`, all other validators will be skipped even when the value is `null`. `false` by default.
 * @param validationOptions {@link ValidationOptions}
 *
 * @see IsOptional exported from `class-validator.
 */
export function IsOptional(
  nullable = false,
  validationOptions?: ValidationOptions,
) {
  if (nullable) {
    return IsOptionalValidator(validationOptions);
  }

  return ValidateIf((ob: any, v: any) => {
    return v !== undefined;
  }, validationOptions);
}
Alex
  • 1,623
  • 1
  • 24
  • 48
0

That's the limitation of the library, it doesn't allow condition branching. The best way is to write your own validator that allows only nulls.

satanTime
  • 12,631
  • 1
  • 25
  • 73