Validating Enum by name #2980
Replies: 10 comments 15 replies
-
My workaround is to create a custom type that is a subclass of from enum import Enum
import pydantic.json
class EnumByName(Enum):
"""A custom Enum type for pydantic to validate by name.
"""
# The reason for this class is there is a disconnect between how
# SQLAlchemy stores Enum references (by name, not value; which is
# also how we want our REST API to exchange enum references, by
# name) and Pydantic which validates Enums by value. We create a
# Pydantic custom type which will validate an Enum reference by
# name.
# Ugliness: we need to monkeypatch pydantic's jsonification of Enums
pydantic.json.ENCODERS_BY_TYPE[Enum] = lambda e: e.name
@classmethod
def __get_validators__(cls):
# yield our validator
yield cls._validate
@classmethod
def __modify_schema__(cls, schema):
"""Override pydantic using Enum.name for schema enum values"""
schema['enum'] = list(cls.__members__.keys())
@classmethod
def _validate(cls, v):
"""Validate enum reference, `v`.
We check:
1. If it is a member of this Enum
2. If we can find it by name.
"""
# is the value an enum member?
try:
if v in cls:
return v
except TypeError:
pass
# not a member...look up by name
try:
return cls[v]
except KeyError:
name = cls.__name__
expected = list(cls.__members__.keys())
raise ValueError(f'{v} not found for enum {name}. Expected one of: {expected}') |
Beta Was this translation helpful? Give feedback.
-
Just to clarify the need for this, this is actually quite a common use case. The use case is when I want to accept coded values for a field (classic Enum use-case) but internally I want the exact machine equivalent that's stored in the Enum value. And again, when returning a response, the user doesn't want to / doesn't need to / shouldn't receive the internal values which may be an implementation detail anyway. They want to get the same codes back. From one related question I just found on Stack Overflow:
The user should provide "user", "manager", etc in the request. The user should see "user", "manager", etc in the serialised JSON response. Internally I want to see the I would image it would be right to target being able to choose whether names or values are used on both input and output, although |
Beta Was this translation helpful? Give feedback.
-
Hi, from enum import IntEnum
from typing import Annotated
import pytest
from pydantic import BaseModel, BeforeValidator, ValidationError
class LogLevel(IntEnum):
DEBUG = 1
INFO = 2
def log_level_by_name(v: str) -> LogLevel:
try:
return LogLevel[v]
except KeyError:
raise ValueError("invalid value")
LogLevelByName = Annotated[LogLevel, BeforeValidator(log_level_by_name)]
class AppConfig(BaseModel):
log_level: LogLevelByName
def test_enum():
c = AppConfig.model_validate({"log_level": "DEBUG"})
assert c.log_level is LogLevel.DEBUG
with pytest.raises(ValidationError):
c = AppConfig.model_validate({"log_level": 1}) Obviously this is not very usable as it requires a new validator function for each enum type. |
Beta Was this translation helpful? Give feedback.
-
When you consider the fact that Python offers the class MyEnum(enum.Enum):
foo = enum.auto()
bar = enum.auto()
baz = enum.auto() I would obviously want this enum to be serialized and deserialized by name rather than by value. Use of It would be great if Pydantic recognized that enums with a static name and dynamic value are actually very common, and would provide built-in support for this scenario so we didn't have to resort to implementing our own custom validators and serializers. |
Beta Was this translation helpful? Give feedback.
-
im playing around with something like this
but then to transfer it back to the origional enum |
Beta Was this translation helpful? Give feedback.
-
I'm converting an app to FastAPI and for pretty much all my usage I consider the value of an enum to be an internal thing that should never appear in the API calls. Two examples to illustrate:
If I later switch that to:
I want the API clients to have to change their calls - if they just pass 1, 2, 3, then they may get the wrong colors with no indication of errors.
All I want in the API is:
With the value implementation it appears I have to pass some mangled version of the entire value dictionary. Of course I can fix all these problems for each individual case, but a global config flag to just use name instead of value would be so much simpler. I had a quick hack at
Certainly not tested all cases, and this doesn't fix serialisation, but seems to suggest it would not be a huge change. |
Beta Was this translation helpful? Give feedback.
-
My solution for this case: import enum
from typing import Any
from pydantic import GetCoreSchemaHandler
from pydantic_core import core_schema
def pydantic_enum[E: enum.Enum](enum_cls: type[E]) -> type[E]:
def __get_pydantic_core_schema__(cls: type[E], source_type: Any, handler: GetCoreSchemaHandler):
assert source_type is cls
def get_enum(value: Any, validate_next: core_schema.ValidatorFunctionWrapHandler):
if isinstance(value, cls):
return value
else:
name: str = validate_next(value)
return enum_cls[name]
def serialize(enum: E):
return enum.name
expected = [member.name for member in cls]
name_schema = core_schema.literal_schema(expected)
return core_schema.no_info_wrap_validator_function(
get_enum, name_schema,
ref=cls.__name__,
serialization=core_schema.plain_serializer_function_ser_schema(serialize)
)
setattr(enum_cls, '__get_pydantic_core_schema__', classmethod(__get_pydantic_core_schema__))
return enum_cls Usage example: from pydantic import BaseModel, ValidationError
@pydantic_enum
class MyEnum(enum.Enum):
biba = enum.auto()
boba = enum.auto()
class Owner(BaseModel):
value: MyEnum
assert Owner.model_validate({
'value': 'biba'
}).value == MyEnum.biba
assert Owner.model_validate({
'value': MyEnum.boba
}).value == MyEnum.boba
try:
Owner.model_validate({
'value': 1
})
assert False
except ValidationError:
pass
assert Owner(value=MyEnum.biba).model_dump(mode='json') == {'value': 'biba'} Unfortunately, it won't work with FastAPI response because of this |
Beta Was this translation helpful? Give feedback.
-
This also means that Pydantic does not support enum aliases. class Shape(Enum):
SQUARE = 2
DIAMOND = 1
CIRCLE = 3
ALIAS_FOR_SQUARE = 2 If an API returns The OpenAPI Schema also contains the duplicate value:
|
Beta Was this translation helpful? Give feedback.
-
My solution to the problem is this: class EnumByName:
def __init__(self, *, ignore_case: bool = True):
self.ignore_case = ignore_case
def __get_pydantic_core_schema__(self, enum_cls: type[Enum], _handler: GetCoreSchemaHandler):
name_enum = Enum('name_enum', {member.name: member.name for member in enum_cls})
name_enum = cast(type[Enum], name_enum)
def enum_or_name(value: Enum | str) -> Enum:
if isinstance(value, str):
if not self.ignore_case:
try:
return enum_cls[value]
except KeyError:
raise ValueError(f'Enum name not found: {value}')
try:
return next(
member for member in enum_cls if member.name.lower() == value.lower()
)
except StopIteration:
raise ValueError(f'Enum name not found: {value}')
elif isinstance(value, enum_cls):
return value
raise ValueError(f'Expected enum member or name, got {type(value).__name__}: {value}')
return core_schema.no_info_plain_validator_function(
enum_or_name,
json_schema_input_schema=core_schema.enum_schema(
enum_cls, list(name_enum.__members__.values())
),
ref=enum_cls.__name__,
serialization=core_schema.plain_serializer_function_ser_schema(lambda e: e.name),
)
class MyEnum(Enum):
A = 1
B = 2
C = 3
class MyModel(BaseModel):
my_enum: Annotated[MyEnum, EnumByName()] It has a few advantages:
|
Beta Was this translation helpful? Give feedback.
-
I generally want this behavior globally, not opt-in. This seems to work well: class ByNameEnumGenerateSchema(GenerateSchema):
def match_type(self, obj: Any) -> core_schema.CoreSchema:
if inspect.isclass(obj) and issubclass(obj, enum.Enum):
def get_enum(
value: Any, validate_next: core_schema.ValidatorFunctionWrapHandler
) -> Any:
if isinstance(value, obj):
return value
else:
name: str = validate_next(value)
return obj[name]
def serialize(
value: enum.Enum, info: core_schema.SerializationInfo
) -> str | enum.Enum:
if info.mode == "json":
return value.name
return value
expected = [member.name for member in obj]
name_schema = core_schema.literal_schema(expected)
return core_schema.no_info_wrap_validator_function(
get_enum,
name_schema,
ref=obj.__qualname__,
serialization=core_schema.plain_serializer_function_ser_schema(
serialize, info_arg=True
),
)
return super().match_type(obj) class CustomModel(BaseModel):
model_config = ConfigDict(
schema_generator=ByNameEnumGenerateSchema, defer_build=True
) Then just use |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I have not been able to find this in a Discussion or Issue...
If I have a python Enum:
I can use this with SQLAlchemy...
On saving, sqlalchemy uses name NOT the value. That is, sqlalchemy will store "NY", not "New York". Similarly, via my REST API, I want to accept the "codes" not the (possibly) long strings. This is a disconnect from pydantic which validates by value.
I have seen answers to similar questions on stackoverflow suggest using the same value as name, e.g:
But this is not practical when you want to offer your Enum as a CODE=>HUMAN_READABLE mapping.
In short, what I want is a Config option, e.g.,
validate_enum_by_name
(for backward compatibility, default toFalse
):Beta Was this translation helpful? Give feedback.
All reactions