Map a JSON Key to a Field
=========================
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 ` 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 ` 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_extensions 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 Union
from typing_extensions import Annotated
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