File: custom_key_mappings.rst

package info (click to toggle)
dataclass-wizard 0.35.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,808 kB
  • sloc: python: 15,276; makefile: 111; javascript: 23
file content (226 lines) | stat: -rw-r--r-- 7,717 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
Map a JSON Key to a Field
=========================

.. note::
    **Important:** The current *key transform* and "custom mappings" functionality is being phased out.
    Please refer to the new docs for **V1 Opt-in** features, which introduces enhanced support for these use
    cases. For more details, see the `Field Guide to V1 Opt‐in`_ and the `V1 Alias`_ documentation.

    This change is part of the ongoing improvements in version ``v0.35.0+``, and the old functionality will no longer be maintained in future releases.

.. _Field Guide to V1 Opt‐in: https://github.com/rnag/dataclass-wizard/wiki/Field-Guide-to-V1-Opt%E2%80%90in
.. _V1 Alias: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/v1_alias.html

The ``dataclass-wizard`` library provides a set of built-in *key transform* helper
functions that automatically transform the casing of keys in a JSON or Python
``dict`` object to and from dataclass field names. As mentioned in the
:doc:`Meta <meta>` section, this key transform only applies to dataclasses
at present, not to keys in ``dict`` objects or to sub-classes of
`NamedTuple`_ or `TypedDict`_, for example.

When converting a JSON key to a dataclass field name, the key transform function
defaults to :func:`to_snake_case`, which converts all JSON keys to -
*you guessed it!* - `snake case`_, which is the leading convention in Python. Therefore, a JSON key
appearing as *myField*, *MYField*, *MyField*, or *my-field* will all implicitly
be mapped to a dataclass field named ``my_field`` by default. When converting
the dataclass field back to JSON, the default key transform function is
:func:`to_camel_case`, which transforms it back to ``myField`` in this case.
It's also possible to update the key transform functions used, as explained in
the :doc:`Meta <meta>` section.

However, suppose you want to instead create a custom mapping of a JSON key to a
dataclass field name. For example, a key appears in the JSON object as
``myJSONKey`` (case-sensitive), and you want to map it to a dataclass
field that is declared as ``my_str``.

The below example demonstrates how to set up a custom mapping of a JSON key name
to a dataclass field. There a few different options available, so feel free to
choose whichever approach is most preferable. I am myself partial to the last
approach, as I find it to be the most explicit, and also one that plays well
with IDEs in general.

.. note:: The mapping of JSON key to field below is only in *addition* to the
  default key transform as mentioned above. For example, ``myNewField`` is already
  mapped to a ``my_new_field`` dataclass field, and the inverse is also true.

.. code:: python3

    from dataclasses import dataclass, field
    from typing import Annotated

    from dataclass_wizard import JSONSerializable, json_field, json_key


    @dataclass
    class MyClass(JSONSerializable):

        # 1-- Define a mapping for JSON key to dataclass field in the inner
        #     `Meta` subclass.
        class Meta(JSONSerializable.Meta):
            json_key_to_field = {
                'myJSONKey': 'my_str'
            }

        # 2-- Using a sub-class of `Field`. This can be considered as an
        #     alias to the helper function `dataclasses.field`.
        my_str: str = json_field(["myField", "myJSONKey"])

        # 3-- Using `Annotated` with a `json_key` (or :class:`JSON`) argument.
        my_str: Annotated[str, json_key('myField', 'myJSONKey')]

        # 4-- Defining a value for `__remapping__` in the metadata stored
        #     within a `dataclasses.Field` class.
        my_str: str = field(metadata={
            '__remapping__': json_key('myField', 'myJSONKey')
        })

One thing to note is that the mapping to each JSON key name is case-sensitive,
so passing *myfield* (all lowercase) will not match a *myField* key in a
JSON or Python ``dict`` object.

In either case, you can confirm that the custom key mapping works as expected:

.. code:: python3

    def main():

        string = """
        {"myJSONKey": "hello world!"}
        """

        c = MyClass.from_json(string)
        print(repr(c))
        # prints:
        #   MyClass(my_str='hello world!')

        print(c)
        # prints:
        #   {
        #     "myStr": "hello world!"
        #   }


    if __name__ == '__main__':
        main()


Map a Field Back to a JSON Key
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, the reverse mapping (dataclass field to JSON key) will not
automatically be associated by default.

You can pass the ``all`` parameter (or an :attr:`__all__` key, in the case
of a dictionary) to also associate the inverse mapping, as shown below.

.. note:: If multiple JSON keys are specified for a dataclass field, only
  the first one provided will be used to map a field name to a JSON key.

Using the :class:`Meta` approach
--------------------------------

.. code:: python3

    from typing import Union
    from dataclasses import dataclass

    from dataclass_wizard import JSONSerializable


    @dataclass
    class MyClass(JSONSerializable):

        class Meta(JSONSerializable.Meta):

            json_key_to_field = {
                # Pass `__all__` so the inverse mapping is also added.
                '__all__': True,
                # If there are multiple JSON keys for a field, the one that is
                # first defined is used in the dataclass field to JSON key mapping.
                'myJSONKey': 'my_str',
                'myField': 'my_str',
                'someBoolValue': 'my_bool',
            }

        my_str: str
        my_bool: Union[bool, str]

Using a :func:`dataclasses.Field` subclass
------------------------------------------

.. code:: python3

    from typing import Union
    from dataclasses import dataclass

    from dataclass_wizard import JSONSerializable, json_field


    @dataclass
    class MyClass(JSONSerializable):
        my_str: str = json_field(
            ('myJSONKey',
             'myField'),
            # Pass `all` so the inverse mapping is also added.
            all=True
        )

        my_bool: Union[bool, str] = json_field(
            'someBoolValue', all=True
        )

Using Annotated with a :func:`json_key` argument
------------------------------------------------

.. code:: python3

    from dataclasses import dataclass
    from typing import Annotated, Union

    from dataclass_wizard import JSONSerializable, json_key


    @dataclass
    class MyClass(JSONSerializable):

        my_str: Annotated[str,
                          # If there are multiple JSON keys listed for a
                          # dataclass field, the one that is defined first
                          # will be used.
                          json_key('myJSONKey', 'myField', all=True)]

        my_bool: Annotated[Union[bool, str],
                           json_key('someBoolValue', all=True)]


In all the above cases, the custom key mappings apply for both the *load*
and *dump* process, so now the below behavior is observed:

.. code:: python3

    def main():

        string = """
        {"myJSONKey": "hello world!", "someBoolValue": "TRUE"}
        """

        c = MyClass.from_json(string)
        print(repr(c))
        # prints:
        #   MyClass(my_str='hello world!', my_bool='TRUE')

        print(c)
        # prints:
        #   {
        #     "myJSONKey": "hello world!",
        #     "someBoolValue": "TRUE"
        #   }


    if __name__ == '__main__':
        main()


.. _NamedTuple: https://docs.python.org/3.8/library/typing.html#typing.NamedTuple
.. _TypedDict: https://docs.python.org/3.8/library/typing.html#typing.TypedDict
.. _snake case: https://en.wikipedia.org/wiki/Snake_case