| # Copyright 2024 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Deserialize python dataclass from json. | |
| This is a minimal drop-in replacement from dataclasses_json. | |
| Dataclasses annotated with `@dataclass_json` gets a `from_dict` factory class method. | |
| Members can be renamed with `field` metadata: | |
| from dataclasses import dataclass, field | |
| @dataclass_json | |
| @dataclass | |
| class Process: | |
| koid: int | |
| name: str | |
| vmos_koid: List[int] = field(metadata=config(field_name="vmos")) | |
| The configuration also makes possible to override the conversion from json to the field. | |
| """ | |
| import dataclasses | |
| import types | |
| import typing | |
| from collections import abc | |
| from enum import Enum | |
| from typing import Any, Callable, ForwardRef, Mapping | |
| class Undefined(Enum): | |
| """Missing fields always fails. Added for API compatibility.""" | |
| RAISE = 1 | |
| def config( | |
| field_name: str | None = None, | |
| decoder: Callable[[Any], Any] | None = None, | |
| ) -> Mapping[str, Any]: | |
| """Returns a field metadata dictionary that override the default field name and deserialization. | |
| Args: | |
| field_name: JSON object field name holding the data for this field. The dataclass field name | |
| is used by default. | |
| decoder: function receiving the JSON value and returning a value to be assigned to the | |
| dataclass field. `_default_decoder` is used by default. | |
| Returns: | |
| """ | |
| return dict(field_name=field_name, decoder=decoder) | |
| def _identity(x: Any) -> Any: | |
| return x | |
| def _default_decoder(cls: type, field_type: Any) -> Callable[[Any], Any]: | |
| origin = typing.get_origin(field_type) | |
| if origin is list or origin is abc.Sequence: | |
| inner_types = typing.get_args(field_type) | |
| assert len(inner_types) == 1, f"Supports only list with one type" | |
| inner_decoder = _default_decoder(cls, inner_types[0]) | |
| return lambda json_data: [inner_decoder(e) for e in json_data] | |
| # TODO(https://github.com/python/mypy/issues/21588): Use typing.Union | |
| # instead of types.UnionType once typeshed (python/typeshed#13740) and | |
| # mypy support it in isinstance(). | |
| elif isinstance(field_type, types.UnionType): | |
| inner_type, likely_null = typing.get_args(field_type) | |
| assert likely_null is type(None), "Supports only Optional" | |
| return _default_decoder(cls, inner_type) | |
| elif dataclasses.is_dataclass(field_type): | |
| assert hasattr(field_type, "from_dict"), ( | |
| f"Class {field_type} does not have 'from_dict'. " | |
| "Did you annotated with `@dataclass_json()`" | |
| ) | |
| return field_type.from_dict | |
| elif isinstance(field_type, ForwardRef): | |
| resolved_decoder: Callable[[Any], Any] | None = None | |
| def decode_forward_ref(x: Any) -> Any: | |
| nonlocal resolved_decoder | |
| if resolved_decoder is None: | |
| resolved_decoder = _default_decoder( | |
| cls, field_type.evaluate(owner=cls) | |
| ) | |
| return resolved_decoder(x) | |
| return decode_forward_ref | |
| else: | |
| return _identity | |
| def dataclass_json( | |
| undefined: Undefined | None = None, | |
| ) -> Callable[[type], type]: | |
| """Annotates a dataclass, adding a `from_dict` method that deserialises a json value.""" | |
| del undefined | |
| def decorator(cls: type) -> type: | |
| assert dataclasses.is_dataclass(cls) | |
| json_to_py = dict() | |
| for field in dataclasses.fields(cls): | |
| json_key = field.metadata.get("field_name", field.name) | |
| decoder = field.metadata.get("decoder") or _default_decoder( | |
| cls, field.type | |
| ) | |
| json_to_py[json_key or field.name] = (field.name, decoder) | |
| def from_dict(data_dict: Mapping[str, Any]) -> Any: | |
| kws = dict() | |
| for key, value in data_dict.items(): | |
| try: | |
| field_name, decoder = json_to_py[key] | |
| kws[field_name] = decoder(value) | |
| except Exception as e: | |
| raise ValueError( | |
| f"Unable to decode field {key!r} of {cls.__name__!r}" | |
| ) from e | |
| return cls(**kws) | |
| assert not hasattr(cls, "from_dict") | |
| cls.from_dict = from_dict # type: ignore[attr-defined] | |
| return cls | |
| return decorator |