lidatong / dataclasses-json

Easily serialize Data Classes to and from JSON
MIT License
1.39k stars 154 forks source link

Improvement request: to_dict is using the field_name instead of the attr name #171

Closed nazariyv closed 4 years ago

nazariyv commented 4 years ago
@dataclass_json
@dataclass
class Person:
    name = field(metadata=config(field_name="NAME"))

Person.from_dict({"NAME": "Ray Dalio"}).to_dict()

expected:

{"name": "Ray Dalio"}

actual:

{"NAME": "Ray Dalio"}

would be nice to have a switch somewhere that allows us to use the attr name instead of the field_name. My current solution is an additional decorator that monkeypatches the to_dict function:

def dataclass_to_dict_enriched(cls):
    """Monkeypatches the dataclass and dataclass_jason instance's to_dict.
    Problem: when calling .to_dict() on the instance, we get back the metadata field_name instead of the
    desired attribute_name! This decorator will monkeypatch the to_dict so that it returns the attribute name.

    Returns:
        [type] -- [description]
    """
    # ensure was decorated with dataclass. does not check the dataclass_json, but requires it. if not decorated, will just return default .to_dict
    def enriched_to_dict(field_map: dict, old_to_dict: Callable):
        @wraps(old_to_dict)
        def enriched(*args, **kwargs):
            r = old_to_dict(*args, **kwargs)
            for field_name, attr_name in field_map.items():
                r[attr_name] = r[field_name]
                del r[field_name]
            return r

        return enriched

    def monkeypatch(cls):
        if not all(
            hasattr(cls, attr) for attr in ["__annotations__", "__dataclass_fields__"]
        ):
            logger.error(
                "could not find required dunder methods. looks like cls is not a dataclass and dataclass_json instance"
            )
        try:
            attr_field_names = {}
            all_fields = cls.__annotations__
            for field in all_fields:
                meta = cls.__dataclass_fields__[field].metadata.get("dataclasses_json")
                if meta and "letter_case" in meta:
                    field_name = meta["letter_case"].__defaults__[0]
                    attr_field_names[field_name] = field
            # now need to run the original to_dict, and then apply the map to get back the attr names instead of the field names
            cls.to_dict = enriched_to_dict(attr_field_names, cls.to_dict)
        except Exception as e:
            logger.exception(f"could not enrich the to_dict. here is the error: {e}")
        return cls

    cls = monkeypatch(cls)
    return cls
sunbit commented 4 years ago

IMO, there's nothing wrong with that:

field_name defines how the attribute will be named on the outside world domain, and the attribute name defines the same inside your application logic domain. So if you're using it for both encoding and decoding, you want to keep consistency of that names in each domain.

If you need to reencode to json with different key names that in the decode input data, then you probably have two different domains that need to be modeled separately with different dataclasses.