Forbid specific enum value for DTO in Nestjs

The problem is that when I send "RUNNING" I'm still getting a 200 instead of a 400.

it seems that you are using the string(!) "RUNNING" as value in your request payload as such:

{ appState: "RUNNING" }

In this case, IsEnum and NotEquals both regard the payload as valid.

Why is that?

First of all numeric enums are reverse mapped by typescript so your enum is internally (as javascript object) represented as follows:

{
  '0': 'SUCCESS',
  '1': 'ERROR',
  '2': 'RUNNING',
  'SUCCESS': 0,
  'ERROR': 1,
  'RUNNING': 2
}

Now class-validator's isEnum() is coded as follows:

isEnum(value: unknown, entity: any): boolean {
    const enumValues = Object.keys(entity)
        .map(k => entity[k]);
    return enumValues.indexOf(value) >= 0;
}

and since the enum is reverse-mapped isEnum('RUNNNING', AppState) will return true.

At the same time NotEquals, which is coded as such...

notEquals(value: unknown, comparison: unknown): boolean {
    return value !== comparison;
}

will compare the string 'RUNNING' against AppState.RUNNING (which equates to 2) and also conclude that this is valid since 'RUNNING' != 2.

So there you have it why the payload { appState: "RUNNING" } will result in a 200 instead of a 400 status code.

How can I prevent this behaviour?

The enum value AppState.RUNNING equates to 2 so when you make a request, you should use the numeric value of 2 in your payload:

{ appState: 2 }

In the above case, the class-validator's NotEquals validator will then correctly deny the request with the response containing:

"constraints": {
    "notEquals": "appState should not be equal to 2"
}

I assume you are sending in the string 'RUNNING', and you're trying to make sure that that is what is not used, correct? With what you've currently got, your enum maps to these values:

export enum AppState {
  SUCCESS = 0,
  ERROR = 1,
  RUNNING = 2
}

So if you send in the string 'RUNNING', the validator checks that RUNNING !== 2 which is in fact true leading to successful validation. The @IsEnum() decorator checks that the value sent in in a valid key of the enum, so sending in 'RUNNING' passes that check, hence why you don't get some sort of error there.

The most verbose way to fix this is to make your enum a string enum like so:

export enum AppState {
  SUCCESS = 'SUCCESS',
  ERROR = 'ERROR',
  RUNNING = 'RUNNING'
}

This will make each AppState value map to its corresponding string, though that does lead to having to type out a lot of declarations and can lead to duplicate code.

Another way to manage this is to set your @NotEquals() enum to the key provided by the enum value like so:

export class UpdateAppStateDTO {
  @IsEnum(AppState)
  @NotEquals(AppState[AppState.RUNNING])
  public appState: AppState;
}

But keep in mind that with this approach when you look at appState later it will still be a numeric value instead of a string.

You can play around with this stackblitz I made for this to see some running code.