How to map a field to different key for serilization and de-serilization with Python dataclass

67 views Asked by At

I want to serialize and de-serialize some data with a dataclass, but one field in the received data has a different name than the class attribute I want to map it to for example in the data I have : {"accountId":123,"shortKey": 54} and I want to map it to a dataclass like:

from dataclasses import dataclass
from dataclasses import field

@dataclass
class MySchema:
    account_id=field(default=None, metadata=dict(required=False))
    short_key=field(default=None, metadata=dict(required=False))

but I want to map between accountId<->account_id and shortKey<->short_key. How can this be done. I saw the data_key option but I read conflicting things whether this works for serilization and de-serialization. What is the way to do it

1

There are 1 answers

0
jsbueno On

It is likely that libraries/frameworks for serialization, like "marshmallow" already do this.

However, it is just a matter of adding an extra function call on the pipeline to serialize/de-serialize stuff -

Say, we build a class which takes in the mapping rules and add a to_python and a from_python methods:

from collections.abc import Mapping, Sequence

def list_sequence(obj):
    return isinstance(obj, Sequence) and not isinstance(obj, (str, bytes, bytearray))

class Transform:
    def __init__(self, to_python_map: Mapping[str:str]):
         self.to_python_map = to_python_map.copy()
         self.from_python_map = {value:key for key, value in to_python_map.items()}

    @staticmethod
    def _remap(rules, data, recurse):
        if not callable(rules):
            rules = lambda key, _map=rules: _map.get(key, key) # second "key" is used as default value
        result = data 
        if list_sequence(data) and recurse:
            result = [Transform._remap(rules, item, recurse) for item in data]
        elif isinstance(data, Mapping):
            result = {}
            for key, value in data.items():
                if recurse and (isinstance(value, Mapping) or list_sequence(value)):
                    value = Transform._remap(rules, value, recurse)
                result[rules(key)] = value
        return result
        

    def to_python(self, data, recurse=True):
        return self._remap(self.to_python_map, data, recurse)

    def from_python(self, data, recurse=True):
        print(data)
        return self._remap(self.from_python_map, data, recurse)
    
    def serializer(self, data):
        return self.from_python(dict(data), recurse=False)


And then, this can be used either with dataclasses.asdict, or as an outer call wrapping json.dumps or json.loads. Also, I wrote it so "rules" can be a callable, and it would be easy to write a function to map all snake_case to CamelCase and use it with this class.

(Note I needed the helper list_sequence call, because strings are "Sequences" but not ones we want apply these rules on all items)

In[109] mapper = Transform({"DataKey": "data_key"})


In [110] @dataclass
         class Test:
            data_key: str

In [111]: Test(**mapper.to_python(json.loads('{"DataKey": "hello"}')))
Out[111]: Test(data_key='hello')

In [112]: dataclasses.asdict(t, dict_factory=mapper.serializer)
{'data_key': 'hello'}
Out[112]: {'DataKey': 'hello'}