yukinarit / pyserde

Yet another serialization library on top of dataclasses, inspired by serde-rs.
https://yukinarit.github.io/pyserde/guide/en
MIT License
726 stars 40 forks source link

Enum deserialization doesn't work with non-simple enum values #585

Open morrison12 opened 2 months ago

morrison12 commented 2 months ago

Enums with "complex" values, for example a tuple, can't be deserialized as shown from the example below. In the example below, it appears it is because the values are deserialized as alist but only a tuple is permitted !?! If the name and not the value(s) were serialized for Enums this could be avoided but would break the cases where the value is desired. Perhaps serialize the name for all but IntEnum and StrEnum ?

Example

"""
Example of serializing and deserializing an Enum with a "complex" value
"""

from dataclasses import dataclass
from enum import Enum, unique

from serde import serde
from serde.json import from_json, to_json

@unique
class B(Enum):
    X = ("one", "some info on one")
    Y = ("two", "some info on two")
    Z = ("three", "some info on three")

@serde
@dataclass
class C:
    m: B

original = C(B.Y)

as_json = to_json(original)
print(f"{as_json=}")
print(f"{B.Z.value=}")
nb = B(("three", "some info on three"))
print(f"{nb.value=}")
from_json = from_json(C, as_json)
print("de-serialized as:", as_json)

assert original == from_json

Output

as_json='{"m":["two","some info on two"]}'
B.Z.value=('three', 'some info on three')
nb.value=('three', 'some info on three')
Traceback (most recent call last):
  File "/Users/james/Projects/spt/examples/ts9.py", line 32, in <module>
    from_json = from_json(C, as_json)
                ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.virtualenvs/spt/lib/python3.11/site-packages/serde/json.py", line 105, in from_json
    return from_dict(c, de.deserialize(s, **opts), reuse_instances=False)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.virtualenvs/spt/lib/python3.11/site-packages/serde/de.py", line 533, in from_dict
    return from_obj(cls, o, named=True, reuse_instances=reuse_instances)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.virtualenvs/spt/lib/python3.11/site-packages/serde/de.py", line 501, in from_obj
    raise SerdeError(e) from None
serde.compat.SerdeError: failed to coerce the field C.m value ['two', 'some info on two'] into B: ['two', 'some info on two'] is not a valid B

Environment Details

    pyserde==0.20.1
    Python 3.11.9
    Os: macOS-12.7.5-arm64-arm-64bit
    Machine: arm64
yukinarit commented 2 months ago

@morrison12 currently non-primitive enum value is not supported.

    def enum(self, arg: DeField[Any]) -> str:
        return f"{typename(arg.type)}({self.primitive(arg)})"

https://github.com/yukinarit/pyserde/blob/main/serde/de.py#L936-L937

I will take a look if I can easily support non-primitive enum value

yukinarit commented 2 months ago

hmm currently pyserde relies on enum constructor for deserialization, but to support non-primitive enum value such as tuple requires to generate (de)serialize functions for enum. It is a quite bit of work..

morrison12 commented 2 months ago

hmm currently pyserde relies on enum constructor for deserialization, but to support non-primitive enum value such as tuple requires to generate (de)serialize functions for enum. It is a quite bit of work..

The one thought I had was to establish the convention (for pyserde) that only simple enum's serialize their value and the rest serialize their name and then on the deserialization side it would be only a matter of choosing Enum(x) or Enum[x] as the constructor depending on whether it was a "simple" enum or not. Simple would be presumably be int, float, complex, str, bool: but I don't know how simple, pardon the pun, it is to determine the value type on the deserialization side (esp. with things like Flag).

morrison12 commented 2 months ago

@morrison12 currently non-primitive enum value is not supported.

    def enum(self, arg: DeField[Any]) -> str:
        return f"{typename(arg.type)}({self.primitive(arg)})"

https://github.com/yukinarit/pyserde/blob/main/serde/de.py#L936-L937

I will take a look if I can easily support non-primitive enum value

The workaround isn't too horrible, something like:


class A(Enum):
...

@serde
@dataclass
class B:
....
   foo: A = serde_field(serializer=lambda x:x.name, deserializer=lambda x:A[x])