typestack / class-validator

Decorator-based property validation for classes.
MIT License
11.02k stars 800 forks source link

feat: @IsOptional should works only for undefined values #491

Open vsternbach opened 4 years ago

vsternbach commented 4 years ago

This code:

import { IsNotEmpty, IsOptional, IsString, validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

const PATCH = 'patch';
const POST = 'post';

export class Test {
  @IsOptional({ groups: [PATCH] })
  @IsNotEmpty({ always: true })
  @IsString()
  name: string;
}

async function getValidationErrors(obj, group) {
  return await validate(plainToClass(Test, obj), { groups: [group] });
}

describe('Test', () => {
  it('should fail on post without name', async () => {
    const errors = await getValidationErrors({}, POST);
    expect(errors).not.toEqual([]);
  });
  it('should fail on post when name is undefined', async () => {
    const errors = await getValidationErrors({ name: undefined }, POST);
    expect(errors).not.toEqual([]);
  });
  it('should fail on post when name is null', async () => {
    const errors = await getValidationErrors({ name: null }, POST);
    expect(errors).not.toEqual([]);
  });
  it('should fail on post when name is empty', async () => {
    const errors = await getValidationErrors({ name: '' }, POST);
    expect(errors).not.toEqual([]);
  });
  it('should succeed on patch without name property', async () => {
    const errors = await getValidationErrors({}, PATCH);
    expect(errors).toEqual([]);
  });
  it('should fail on patch when name is undefined', async () => {
    const errors = await getValidationErrors({ name: undefined }, PATCH);
    expect(errors).not.toEqual([]);
  });
  it('should fail on patch when name is null', async () => {
    const errors = await getValidationErrors({ name: null }, PATCH);
    expect(errors).not.toEqual([]);
  });
  it('should fail on patch when name is empty', async () => {
    const errors = await getValidationErrors({ name: '' }, PATCH);
    expect(errors).not.toEqual([]);
  });
});

gives the following results:

Screen Shot 2019-12-19 at 18 15 43

Am I missing something here?

Looking at the code of @IsOptional I see that it checks that the property is not undefined or null, that's why the test for empty string is not failing, but in theory shouldn't it check that this property exists on the object?

vlapo commented 4 years ago

I do not understand your problem. I think your test cases are not good. You define property name as optional in case PATCH is in groups.

it('should fail on patch when name is undefined', async () => {
    const errors = await getValidationErrors({ name: undefined }, PATCH);
    expect(errors).not.toEqual([]);
  });
  it('should fail on patch when name is null', async () => {
    const errors = await getValidationErrors({ name: null }, PATCH);
    expect(errors).not.toEqual([]);
  });

So Test should succeed on patch when name is undefined/null. From docs:

@IsOptional() Checks if given value is empty (=== null, === undefined) and if so, ignores all the validators on the property.
Kiyozz commented 4 years ago

I use nestjs to validate a DTO from my controller


I have this

class MyDTO {
  @IsOptional()
  @IsNotEmpty()
  locale?: string
}

I send this body json

{
  "locale": null
}

The expected behaviour:

The actual behaviour:


If I have this DTO

class MyDTO {
  @IsNotEmpty()
  locale?: string
}

Then null value throws CODE 400 which is expected.


@IsOptional() should ignores only when the key is not passed in the request (aka. undefined) or an option to not ignores null value.

vlapo commented 4 years ago

I understand now. But as it is a breaking change I want to have more feedback from community.

jrafaels commented 4 years ago

I don't know if is the same issue, but i need to validate if a variable is notEmpty, notNull and notUndefined. Is that possible?

sam3d commented 4 years ago

@vlapo How about something like an extra config option? The implementation I've got in my fork doesn't break backwards compatibility (as the option defaults to true, which is the default behaviour):

@isOptional({ nullable: false }) // object.hasOwnProperty(property);
@isOptional({ nullable: true }) // value !== undefined && value !== null
@isOptional() // value !== undefined && value !== null

The commit on the fork that adds this behaviour: https://github.com/se-internal/class-validator/commit/5ac9fa0c17ad43ecb0e8642675447b380a54139e

fhp commented 4 years ago

We would love this as well, ether with a nullable flag, or as a separate set of decorators (although naming them can be difficult). @sam3d, your fork looks good to me, would you mind creating a PR?

sam3d commented 4 years ago

Thanks @fhp! Yeah we had originally tried to create a new decorator and must've spent over an hour trying to come up with a good name. Eventually, we decided it made more sense to borrow the design pattern from typeorm's @ManyToOne() decorator (a nullable property that defaults to true).

I'll definitely put it into a PR at some point in the next couple of days! Just waiting until I have a moment to add tests. Confirmation from @vlapo or another maintainer that at least the design proposal is accepted would be amazing so I know I'm not doing it for nothing šŸ˜„

omidh28 commented 4 years ago

Any update for this? really want to see this feature in next release @sam3d

NoNameProvided commented 4 years ago

I agree about the strange behavior of this.

I like the API proposed by @sam3d. We can use the current behavior as default and add a warning in the changelog about eventually being flipped.

I will take a crack at this at the weekend.

omidh28 commented 4 years ago

@NoNameProvided Any update? 10 days has been passed

NickKelly1 commented 4 years ago

How about keeping @IsOptional as-is and adding new decorators @IsNullable and @IsUndefinable. Any support for this?

ruscon commented 4 years ago
/**
 * Skips validation if the target is null
 *
 * @example
 * ```typescript
 * class TestModel {
 *     @IsNullable({ always: true })
 *     big: string | null;
 * }
 * ```
 */
export function IsNullable(options?: ValidationOptions): PropertyDecorator {
    return function IsNullableDecorator(prototype: object, propertyKey: string | symbol): void {
        ValidateIf((obj): boolean => null !== obj[propertyKey], options)(prototype, propertyKey);
    };
}
/**
 * Skips validation if the target is undefined
 *
 * @example
 * ```typescript
 * class TestModel {
 *     @IsUndefinable({ always: true })
 *     big?: string;
 * }
 * ```
 */
export function IsUndefinable(options?: ValidationOptions): PropertyDecorator {
    return function IsUndefinableDecorator(prototype: object, propertyKey: string | symbol): void {
        ValidateIf((obj): boolean => undefined !== obj[propertyKey], options)(prototype, propertyKey);
    };
}
hfhchan-plb commented 1 year ago

For other parts of TypeScript, "optional" means either the property is not set, or it is set to undefined. In no other places does "optional" imply "nullable", whereas "nullable" itself can sometimes imply "optional" (c.f. NonNullable utility type). The naming of IsOptional is really confusing.

I suggest the following changes to align with the terminology of TypeScript:

  1. @IsOptional() taking a new strict parameter, defaulting to false which allows null for back-compat, and true for denying null. The strict parameter should default to true at some later version and possibly throw if it is set to false.
  2. @IsNullable with a strict parameter defaulting to true which only allows null, and false for also allowing undefined.
mwanago commented 1 year ago

I think

@IsOptional({ nullable: false })

looks great, as suggested by @sam3d @vlapo Are there any plans on implementing that?

mwanago commented 1 year ago

Meanwhile, I suggest creating two separate decorators:

import { ValidateIf } from 'class-validator';

export function CanBeUndefined() {
  return ValidateIf((object, value) => value !== undefined);
}

export function CanBeNull() {
  return ValidateIf((object, value) => value !== null);
}
pigulla commented 1 year ago

In case anyone is interested, I've had the exact same issue and implemented this (alongside quite a few more decorators) in class-validator-extended, specifically Optional() and Nullable().

oscarmeanwell commented 1 year ago

This is still an issue, please can this be updated? Some fantastic solutions have been presented. I am happy to make a PR

fr1sk commented 1 year ago

any update on this one? And what about PartialType which is making all props optional? Can we somehow pass this nullable param to PartialType as well? šŸ¤”

renatocron commented 4 months ago

I also don't like this behavior, it a new function like IsOptionalNonNullable exists, and had integration with the nest/swagger plugin to also be automatically marked as optional in the swagger, would be great.

It leads to behaviors where a Prisma expect only a value or undef (like the typescript type said so) but if the client/frontend send null, leads the backend to error 500 with weird messages

renatocron commented 4 months ago

Hi @pigulla

Is any helper like PartialType in class-validator-extended? I could not find it

Thank you

pigulla commented 4 months ago

@renatocron,

unfortunately not, I consider that out of scope for that library.

oscarmeanwell commented 4 months ago

I solved this myself and posted on StackOverflow , I am happy to make a PR amending IsOptional or adding this to the library.

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

export function IsOptionalNonNullable(data?: {
  nullable: boolean
  validationOptions?: ValidationOptions
}) {
  const { nullable = false, validationOptions = undefined } = data || {}

  if (nullable) {
    // IsOptional allows null
    return IsOptional(validationOptions)
  }

  return ValidateIf((ob: any, v: any) => {
    return v !== undefined
  }, validationOptions)
}

// Example usage 
export class SomeUpdateDTO {
  @IsInt()
  // Param can be undefined, but not null 
  @IsOptionalNonNullable()
  nbOfViews?: number
}

// Example of why IsOptional is a problem
export class SomeOtherUpdateDTO {
  @IsInt()
  @IsOptional()
  // Null is not specified, but IsOptional will allow it! 
  // Could end up nulling a required field in the db
  nbOfViews?: number
}
alexpizarroj commented 1 month ago

@vlapo @NoNameProvided

I came across this exact problem today. I have an API with a field that must be present. The field represents an assigned user id, and I want it to support either an actual id string or null to clear the assignment. For this endpoint in particular, not receiving an assignedUserId field value at all (i.e., undefined) would be strange/ill-defined.

In the meantime, I'm going with the solution here to unblock myself, however I'd love to help in any way I can get something official merged. For example, we already have a great contribution in https://github.com/typestack/class-validator/pull/2443.

Thank you so much for all the work in this project šŸ™šŸ»