diff --git a/apischema/conversions/__init__.py b/apischema/conversions/__init__.py index 36994591..d66ecd74 100644 --- a/apischema/conversions/__init__.py +++ b/apischema/conversions/__init__.py @@ -2,6 +2,7 @@ "AnyConversion", "Conversion", "LazyConversion", + "as_names", "as_str", "dataclass_input_wrapper", "dataclass_model", @@ -15,6 +16,7 @@ from .conversions import AnyConversion, Conversion, LazyConversion from .converters import ( + as_names, as_str, deserializer, inherited_deserializer, diff --git a/apischema/conversions/converters.py b/apischema/conversions/converters.py index 9964dbe5..328b53ed 100644 --- a/apischema/conversions/converters.py +++ b/apischema/conversions/converters.py @@ -2,6 +2,9 @@ import warnings from collections import defaultdict from dataclasses import replace +from enum import Enum +from functools import partial +from types import new_class from typing import ( Callable, Dict, @@ -22,6 +25,7 @@ resolve_conversion, ) from apischema.conversions.utils import Converter, is_convertible +from apischema.type_names import type_name from apischema.types import AnyType from apischema.typing import is_type_var from apischema.utils import ( @@ -244,10 +248,31 @@ def inherited_deserializer(method=None, **kwargs): return InheritedDeserializer(method, **kwargs) -Cls = TypeVar("Cls", bound=Type) +Cls = TypeVar("Cls", bound=type) def as_str(cls: Cls) -> Cls: deserializer(Conversion(cls, source=str)) serializer(Conversion(str, source=cls)) return cls + + +EnumCls = TypeVar("EnumCls", bound=Type[Enum]) + + +def as_names(cls: EnumCls, aliaser: Callable[[str], str] = lambda s: s) -> EnumCls: + # Enum requires to call namespace __setitem__ + def exec_body(namespace: dict): + for elt in cls: # type: ignore + namespace[elt.name] = aliaser(elt.name) + + if not issubclass(cls, Enum): + raise TypeError("as_names must be called with Enum subclass") + name_cls = type_name(None)( + new_class(cls.__name__, (str, Enum), exec_body=exec_body) + ) + deserializer(Conversion(partial(getattr, cls), source=name_cls, target=cls)) + serializer( + Conversion(lambda obj: getattr(name_cls, obj.name), source=cls, target=name_cls) + ) + return cls diff --git a/docs/conversions.md b/docs/conversions.md index 29600bc2..2b752939 100644 --- a/docs/conversions.md +++ b/docs/conversions.md @@ -174,6 +174,14 @@ A common pattern of conversion concerns class having a string constructor and a !!! note Previously mentioned standard types are handled by *apischema* using `as_str`. +## Use `Enum` names + +`Enum` subclasses are (de)serialized using values. However, you may want to use enumeration names instead, that's why *apischema* provides `apischema.conversion.as_names` to decorate `Enum` subclasses. + +```python +{!as_names.py!} +``` + ## Object deserialization — transform function into a dataclass deserializer `apischema.objects.object_deserialization` can convert a function into a new function taking a unique parameter, a dataclass whose fields are mapped from the original function parameters. diff --git a/docs/data_model.md b/docs/data_model.md index d91dca67..e6ae7250 100644 --- a/docs/data_model.md +++ b/docs/data_model.md @@ -71,7 +71,8 @@ They correpond to JSON *object* and are serialized to `dict`. `enum.Enum` subclasses, `typing.Literal` -For `Enum`, this is the value and not the attribute name that is serialized +!!! warning + `Enum` subclasses are (de)serialized using **values**, not names. *apischema* also provides a [conversion](conversions.md#using-enum-names) to use names instead. #### Typing facilities diff --git a/examples/as_names.py b/examples/as_names.py new file mode 100644 index 00000000..de581576 --- /dev/null +++ b/examples/as_names.py @@ -0,0 +1,24 @@ +from enum import Enum + +from apischema import deserialize, serialize +from apischema.conversions import as_names +from apischema.json_schema import deserialization_schema, serialization_schema + + +@as_names +class MyEnum(Enum): + FOO = object() + BAR = object() + + +assert deserialize(MyEnum, "FOO") == MyEnum.FOO +assert serialize(MyEnum, MyEnum.FOO) == "FOO" +assert ( + deserialization_schema(MyEnum) + == serialization_schema(MyEnum) + == { + "$schema": "http://json-schema.org/draft/2019-09/schema#", + "type": "string", + "enum": ["FOO", "BAR"], + } +)