4

I have this application:

import enum
from typing import Annotated, Literal

import uvicorn
from fastapi import FastAPI, Query, Depends
from pydantic import BaseModel

app = FastAPI()


class MyEnum(enum.Enum):
    ab = "ab"
    cd = "cd"


class MyInput(BaseModel):
    q: Annotated[MyEnum, Query(...)]


@app.get("/")
def test(inp: MyInput = Depends()):
    return "Hello world"


def main():
    uvicorn.run("run:app", host="0.0.0.0", reload=True, port=8001)


if __name__ == "__main__":
    main()

curl http://127.0.0.1:8001/?q=ab or curl http://127.0.0.1:8001/?q=cd returns "Hello World"

But any of these

  • curl http://127.0.0.1:8001/?q=aB
  • curl http://127.0.0.1:8001/?q=AB
  • curl http://127.0.0.1:8001/?q=Cd
  • etc

returns 422Unprocessable Entity which makes sense.

How can I make this validation case insensitive?

Chris
  • 18,724
  • 6
  • 46
  • 80
Amin Ba
  • 1,603
  • 1
  • 13
  • 38

2 Answers2

6

You can make enum values case insensitive by overriding the Enum's _missing_ method . As per the documentation, this classmethod—which by default does nothing— can be used to look up for values not found in cls; thus, allowing one to try and find the enum member by value.

Note that one could extend from the str class when declaring the enumeration class (e.g., class MyEnum(str, Enum)), which would indicate that all members in the enum must have values of the specified type (e.g., str). This would also allow comparing a string to an enum member (using the equality operator ==), without having to use the value attribute on the enum member (e.g., if member.lower() == value). Otherwise, if the enumeration class was declared as class MyEnum(Enum) (without str subclass), one would need to use the value attribute on the enum member (e.g., if member.value.lower() == value) to safely compare the enum member to a string.

Also, note that calling the lower() function on the enum member would not be necessary, unless the enum values of your class include uppercase (or a combination of uppercase and lowercase) letters as well. Hence, for the example below, where only lowercase letters are used, you could avoid using it, and instead simply use if member == value to compare the enum member to a value; thus, saving you from calling the lower() funciton on every member in the class.

Example

from enum import Enum

class MyEnum(str, Enum):
    ab = 'ab'
    cd = 'cd'
    
    @classmethod
    def _missing_(cls, value):
        value = value.lower()
        for member in cls:
            if member.lower() == value:
                return member
        return None

Example (in Python 3.11+)

In Python 3.11+, one could instead use the newly introduced StrEnum, which allows using the auto() feature, resulting in the lower-cased version of the member's name as the value.

from enum import StrEnum, auto

class MyEnum(StrEnum):    
    AB = auto()
    CD = auto()
    
    @classmethod
    def _missing_(cls, value):
        value = value.lower()
        for member in cls:
            if member == value:
                return member
        return None
Chris
  • 18,724
  • 6
  • 46
  • 80
  • great. it works. there is a small problem with your answer. If the original vlaues are uppercase or are not lowercase it won't work. maybe you should change it to `def _missing_(cls, value): for member in cls: if member.value.lower() == value.lower(): return member` – Amin Ba Apr 28 '23 at 20:50
  • 1
    Did not use `.lower() ` on `member.value`, as the original code posted in the question used lower case letters for the member values and would be easy for people to figure out and adjust it to their case. But, anyways, thanks for pointing it out - it has now changed. Also, I would not suggest using `== value.lower()`, as shown in your comment above, but rather convert the value to lowercase outside the `for loop`, as otherwise, you would unnecessarily call `.lower()` funcion for every enum member in the loop. – Chris Apr 29 '23 at 03:14
  • another suggestion. changing the class to `MyEnum(str, enum.Enum)` – Amin Ba Apr 29 '23 at 03:16
  • 1
    It's been changed to `(str, Enum)`, but wouldn't be that necessary to do so, as the example above uses the `value` attribute on each enum member to compare the value to a string. However, by doing so, you could also use `if member.lower() == value` instead. Thus, it might prove convenient when it comes to comparing values inside the endpoint as well, as you would not have to use the `value` attribute. Additionally, if any enum members of `MyEnum` class contained values that were not of `str` type (e.g., numbers, dates), they would automatically be converted to `str`. – Chris Apr 29 '23 at 04:08
  • if you are inheriting from str, you should not do `member.value.lower()`. you should do `member.lower()` – Amin Ba Apr 29 '23 at 04:49
  • This works when a string is passed as an argument to MyEnum class but doesn't work with `==`. The fix is to override `__eq__` like this: `def __eq__(self, other: str) -> bool: other = other.lower(); return super().__eq__(other). ` You may also want to add a check that `other` is a string. `isinstance(other, str)`. – Ledorub Jun 28 '23 at 20:06
0

I really like the accepted answer's suggestion, however it can be a little bit simplified (and generalised):

class CaseInsensitiveEnum(str, Enum):
    @classmethod
    def _missing_(cls, value: str):
        for member in cls:
            if member.lower() == value.lower():
                return member
        return None


class MyEnum(CaseInsensitiveEnum):
    ab = 'ab'
    cd = 'cd'

as suggested in previous comments, it can be optimized in 2 ways:

  • member.lower() would not be required, if all enum values will be assigned as lowercase
  • value.lower() can be executed once, outside the for loop
Hunter_71
  • 649
  • 1
  • 9
  • 16