Search code examples
expressvalidationnestjsclass-validator

Validation an object with at least one non-nullable field


Problem:
Backend will receive an object, and at least one field must to be non-nullable.

Example dto object:

export class MessageDto implements TMessage {
    @IsString()
    @MinLength(1)
    roomId: TValueOf<Pick<Room, "id">>;

    @IsArray()
    @MinLength(1, {
        each: true,
    })
    attachmentIds: TValueOf<Pick<File, "id">>[];

    @IsString()
    text: TValueOf<Pick<Message, "text">>;

    @IsOptional()
    @IsString()
    replyToMessageId: TValueOf<Pick<Message, "id">> | null;
}

Explanation - the above-mentioned object may contain neither attachmentIds nor text. I want to be sure that either attachmentIds.length > 0 or text.length > 0.

Question:
Is there a way to validate not only one field, but also the entire object(or fields in couples) using "class-validator" package that is contained by default in NestJs framework? I can't believe, that the "class-validator" package is the bottleneck of the project and I will have to replace it with another one, like "yup" or "zod".

Workaroung:
I just revalidate that object in NestJs.Controller. But I want to make the code clearer.


Solution

  • I will propose the same solution I implemented in my project. The logic is to:

    • A) add an extra field/property which declares the type of input you expect.
    • B) So, you define an enum before with all the possible input values. In this case 1) attachment and 2) text Then, to all the other properties that depend on that, you use the decorator @ValidateIf(), and in every case, depending of the 'inputType' property, the validators work respectively(if they pass the @ValidateIf(condition) ).

    It has to be pointed out, that the 'inputType' cannot be empty, it must be mandatory. Therefore, your goal will be achieved, as to at least one of the possible inputs in NOT empty.

    enum InputType {
        ATTACHMENT = 'attachment',
        TEXT = 'text',
    }
    
    export class MessageDto implements TMessage {
        @IsString()
        @MinLength(1)
        roomId: TValueOf<Pick<Room, "id">>;
    
        @IsEnum(InputType)
        inputType!: InputType;
    
        @ValidateIf((o) => o.inputType === InputType.ATTACHMENT)
        @IsArray()
        @MinLength(1, {
            each: true,
        })
        @IsNotEmpty()
        attachmentIds: TValueOf<Pick<File, "id">>[];
    
        @ValidateIf((o) => o.inputType === InputType.TEXT)
        @IsString()
        @MinLength(1)
        text: TValueOf<Pick<Message, "text">>;
    
        @IsOptional()
        @IsString()
        replyToMessageId: TValueOf<Pick<Message, "id">> | null;
    }