-
-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deserialize json to union of different classes by parameter #67
Comments
Hi @amirotin There would be no problem if I'm going to implement discriminated unions, which will allow choosing the right class based on the value of a field and solve this problem completely. But for now there is a couple of workarounds:
def deserialize_point(value: Dict) -> Union[Point1, Point4]:
pointType = value.get("pointType")
if pointType == 1:
return Point1.from_dict(value)
elif pointType == 4:
return Point4.from_dict(value)
else:
raise ValueError(f"Unknown pointType {pointType}")
@dataclass(slots=True)
class MapData(BaseRequest):
pointList: List[Union[Point1, Point4]]
class Config(BaseConfig):
serialization_strategy = {
Union[Point1, Point4]: {
"deserialize": deserialize_point
}
} |
@Fatal1ty thanks for idea! |
Yes, you can override deserialization of whatever you want. So, you can use |
Another solution is to use @dataclass(slots=True)
class MapPoint(BaseRequest, SerializableType):
x: int
y: int
pointType: int
def _serialize(self):
return self.to_dict()
@classmethod
def _deserialize(cls, value: Dict):
if value.get("pointType") == 1:
return Point1.from_dict(value)
elif value.get("pointType") == 4:
return Point4.from_dict(value)
...
@dataclass(slots=True)
class MapData(BaseRequest):
pointList: List[MapPoint] You can have a registry like |
@Fatal1ty thanks! worked like a charm! |
With a bit of black dirty magic in a simple case a universal deserialization method for all point types would look like this: current_module = sys.modules[__name__]
...
@classmethod
def _deserialize(cls, value: Dict):
pointType = value.get("pointType")
try:
point_cls = getattr(current_module, f'Point{pointType}')
except AttributeError:
raise ValueError(f"Unknown pointType: {pointType}")
return point_cls.from_dict(value) |
If we talk about json, should it be |
It should be |
Thanks again 👍 |
Sounds interesting with a decorator, could you provide an example? |
Yes, sure. It might look something like this: def register_point(point_number: int):
def decorator(class_):
MapPoint.__points__[point_number] = class_
return class_
return decorator
@dataclass(slots=True)
class MapPoint(BaseRequest, SerializableType):
__points__ = {}
...
@classmethod
def _deserialize(cls, value: Dict):
return cls.__points__[value["pointType"]].from_dict(value)
@register_point(1)
@dataclass(slots=True)
class Point1(MapPoint):
pointType: int = 1
...
@dataclass(slots=True)
class MapData(BaseRequest):
pointList: List[MapPoint] Another option is to use @dataclass(slots=True)
class MapPoint(BaseRequest, SerializableType):
__points__ = {}
def __init_subclass__(cls, **kwargs):
MapPoint.__points__[cls.__dict__["pointType"]] = cls
...
@classmethod
def _deserialize(cls, value: Dict):
return cls.__points__[value["pointType"]].from_dict(value)
@dataclass(slots=True)
class Point1(MapPoint):
pointType: int = 1
...
@dataclass(slots=True)
class MapData(BaseRequest):
pointList: List[MapPoint] |
@Fatal1ty cool I like the decorator approach and it works. But can we get rid of the redundant pointType ("1") in either the decorator arg or default variable? |
You can use the def register_point(class_):
MapPoint.__points__[class_.__dict__["pointType"]] = class_
return class_
@register_point
class Point1:
pointType: int = 1 |
I may open source a library I wrote on top of Mashumaro, that also includes serialization extension, that among other things sovles this issue. Here is a snippet from the body of my serialization mixin: def __post_serialize__(self, d: dict[str, Any]) -> dict[str, Any]:
out = {"class": self.__class__.__name__}
for k, v in d.items():
if k.startswith("_"):
continue
out[k] = v
return out
def _serialize(self) -> dict[str, Any]:
if DataClassSerializeMixin.__mashumaro_dialect is not None:
return self.to_dict(dialect=DataClassSerializeMixin.__mashumaro_dialect)
else:
return self.to_dict()
@classmethod
def _deserialize(cls: Type[T], value: dict[str, Any]) -> T:
class_name = value.pop("class", None)
if class_name is None:
raise ValueError("Missing class name ('class' field) in source")
clazz = TYPES.get(class_name, None)
if clazz is None:
raise ValueError(f"Unknown class name: {class_name}")
if DataClassSerializeMixin.__mashumaro_dialect is not None:
return cast(
T, clazz.from_dict(value, dialect=DataClassSerializeMixin.__mashumaro_dialect)
)
else:
return cast(T, clazz.from_dict(value))
def __init_subclass__(cls, **kwargs: Any):
if cls.__name__ in TYPES:
package = "Unknown"
module = inspect.getmodule(TYPES[cls.__name__])
if module is not None:
package = str(module.__package__)
raise ValueError(
f"DataClassSerializeMixin subclass <{cls.__name__}> is already defined in package <{package}>. Please use a different name."
)
TYPES[cls.__name__] = cls
return super().__init_subclass__(**kwargs) It also has some custom to/from_dict/json/msgpack wrappers with more complicated dialects supports, which is not in that snippet. Works like a charm. no decorators necessary |
Hi!
I'm trying to understand if it is possible to deserialize nested dataclass based on some value in main dataclass.
Don't know how to explain it more correctly, probably my code will say more than me:
I've such json:
So i defined dataclass for each pointType i have. serialization worked like a charm, but how can I deserialize such json, choosing correct dataclass for each point?
Fo example make some dict where i can set class for each pointType and pass it to deserialization function. Or this is imposible and i want too much? :)
The text was updated successfully, but these errors were encountered: