12

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?

1

4 Answers 4

21

You could make case insensitive enum values 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 (i.e., member.lower()) would not be necessary, unless the enum member values of your class include uppercase (or a combination of uppercase and lowercase) letters as well (e.g., ab = 'aB', cd = 'Cd', etc.). 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 1

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

Generic Version (with FastAPI example)

from fastapi import FastAPI
from enum import Enum


app = FastAPI()


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

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


@app.get("/")
def main(q: MyEnum):
    return q

In case you needed the Enum query parameter to be defined using Pydantic's BaseModel, you could then use the below (see this answer and this answer for more details):

from fastapi import Query, Depends
from pydantic import BaseModel

...

class MyInput(BaseModel):
    q: MyEnum = Query(...)


@app.get("/")
def main(inp: MyInput = Depends()):
    return inp.q

In both cases, the endpoint could be called as follows:

http://127.0.0.1:8000/?q=ab
http://127.0.0.1:8000/?q=aB
http://127.0.0.1:8000/?q=cD
http://127.0.0.1:8000/?q=CD
...

Example 2

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
Sign up to request clarification or add additional context in comments.

8 Comments

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
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.
another suggestion. changing the class to MyEnum(str, enum.Enum)
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.
if you are inheriting from str, you should not do member.value.lower(). you should do member.lower()
|
4

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

from enum import Enum
from typing import Any


class CaseInsensitiveEnum(str, Enum):
    @classmethod
    def _missing_(cls, value: Any):
        if isinstance(value, str):
            value = value.lower()

            for member in cls:
                if member.lower() == value:
                    return member
        return None


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

member.lower() would not be required when all MyEnum values will be defined as lowercase


@update: I applied @NeilG suggestions from the comment below.

4 Comments

I found mypy reports error: Argument 1 of "_missing_" is incompatible with supertype "Enum"; supertype defines the argument type as "object" [override]. I didn't check but presumably the library has no typing. To suppress the error you can remove the str type for the value argument and instead of value.lower() use str(value).lower().
Thanks for reporting. I updated the answer by applying your suggestions.
How is this generalized, beside the if isinstance(value, str)? Note this value: Any prevents type checkers from detecting mistakes such as CaseInsensitiveEnum(42).
It's generalized by extracting it to the base class, making it reusable by multiple enums definitions without repetitions. And that's the reason why I chose Any for annotation. In the end, there are different use cases and we should always implement a solution that best serves to achieve the final goal. That is why SO questions have multiple answers with different ranks. Some answers are better than others, but sometimes lower-ranked answers are better suitable for the developer, who is looking for some inspiration :) Regards!
2

A more optimal solution can be done by dropping the entire loop with dictionary lookup where the Time-Complexity is O(1) instead of O(n). n is the number if members in the Enum class

class CaseInsensitiveEnum(str, Enum):
    @classmethod
    def _missing_(cls, value: str):
        return cls.__members__.get(value.upper(), None)

I am using this to map bank account type like bellow

class AccountType(str, Enum):
    CHECKING = "Checking"
    SAVINGS = "Saving"

    @classmethod
    def _missing_(cls, value: str):
        # for case insensitive input mapping
        return cls.__members__.get(value.upper(), None)

2 Comments

I can't see anything wrong with this, and it does seem more efficient. Note that the docs show iteration over the class as in the accepted answer. I'm not sure if iterating over the class may be more future proof or have other protections.
@NeilG the difference between using __members__ and iterating of the class is that the former includes aliases, while the latter does not (aliases are entries that share their value with a previous one).
1

If you inherit from StrEnum (from python 3.11), you can use the following decorator:

import enum
import typing

_StrEnumT = typing.TypeVar("_StrEnumT", bound=type[enum.StrEnum])

def case_insensitive(enum_t: _StrEnumT) -> _StrEnumT:
    lowercase_members_mapping = {member.lower(): member for member in enum_t}
    assert len(lowercase_members_mapping) == len(
        enum_t,
    ), f"enum {enum_t.__name__} is case sensitive"

    def _missing_(_cls: typing.Any, value: object) -> typing.Any:
        if not isinstance(value, str):
            raise ValueError(
                f"Invalid value - expected a string, got: {repr(value)}",
            )
        member = lowercase_members_mapping.get(value.lower())
        if member is None:
            raise ValueError(
                f"Invalid {enum_t.__name__}: {repr(value)}",
            )
        return member

    enum_t._missing_ = classmethod(_missing_)

    return enum_t

Like that:

@case_insensitive
class MyEnum(enum.StrEnum):
    ab = "ab"
    cd = "cd"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.