I am using dataclass
to parse (HTTP request/response) JSON objects and today I came across a problem that requires transformation/alias attribute names within my classes.
from dataclasses import dataclass, asdict
from typing import List
import json
@dataclass
class Foo:
foo_name: str # foo_name -> FOO NAME
@dataclass
class Bar:
bar_name: str # bar_name -> barName
@dataclass
class Baz:
baz_name: str # baz_name -> B A Z
baz_foo: List[Foo] # baz_foo -> BAZ FOO
baz_bar: List[Bar] # baz_bar -> BAZ BAR
currently:
# encode
baz_e = Baz("name", [{"foo_name": "one"}, {"foo_name": "two"}], [{"bar_name": "first"}])
json_baz_e = json.dumps(asdict(baz_e))
print(json_baz_e)
# {"baz_name": "name", "baz_foo": [{"foo_name": "one"}, {"foo_name": "two"}], "baz_bar": [{"bar_name": "first"}]}
# decode
json_baz_d = {
"baz_name": "name",
"baz_foo": [{"foo_name": "one"}, {"foo_name": "two"}],
"baz_bar":[{"bar_name": "first"}]
}
baz_d = Baz(**json_baz_d) # back to class instance
print(baz_d)
# Baz(baz_name='name', baz_foo=[{'foo_name': 'one'}, {'foo_name': 'two'}], baz_bar=[{'bar_name': 'first'}])
expected:
# encode
baz_e = Baz("name", [{"FOO NAME": "one"}, {"FOO NAME": "two"}], [{"barName": "first"}])
json_baz_e = json.dumps(asdict(baz_e))
# decode
json_baz_d = {
"B A Z": "name",
"BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}],
"BAZ BAR":[{"barName": "first"}]
}
baz_d = Baz(**json_baz_d) # back to class instance
Is the only solution dataclasses-json, or is there still a possibility without additional libraries?
Modifying fields after initialization with __post_init__ The __post_init__ method is called just after initialization. In other words, it is called after the object receives values for its fields, such as name , continent , population , and official_lang .
A dataclass can very well have regular instance and class methods. Dataclasses were introduced from Python version 3.7. For Python versions below 3.7, it has to be installed as a library.
Python introduced the dataclass in version 3.7 (PEP 557). The dataclass allows you to define classes with less code and more functionality out of the box. The following defines a regular Person class with two instance attributes name and age : class Person: def __init__(self, name, age): self.name = name self.age = age.
DataClass in Python DataClasses are like normal classes in Python, but they have some basic functions like instantiation, comparing, and printing the classes already implemented. Parameters: init: If true __init__() method will be generated. repr: If true __repr__() method will be generated.
Benefits of using dataclass in Python Conclusion What is a dataclass? Dataclass is a decorator defined in the dataclasses module. It was introduced in python 3.7. A dataclass decorator can be used to implement classes that define objects with only data and very minimal functionalities.
Beneath the class Position: line, you simply list the fields you want in your data class. The : notation used for the fields is using a new feature in Python 3.6 called variable annotations. We will soon talk more about this notation and why we specify data types like str and float.
The parameters to dataclass () are: init: If true (the default), a __init__ () method will be generated. If the class already defines __init__ (), this parameter is ignored. repr: If true (the default), a __repr__ () method will be generated.
dataclasses. fields (class_or_instance) ¶ Returns a tuple of Field objects that define the fields for this dataclass. Accepts either a dataclass, or an instance of a dataclass. Raises TypeError if not passed a dataclass or instance of one.
You could certainly use dataclasses-json
for this, however if you don't need the advantage of marshmallow schemas, you can probably get by with an alternate solution like the dataclass-wizard
, which is similarly a JSON serialization library built on top of dataclasses. It supports alias field mappings as needed here; another bonus is that it doesn't have any dependencies outside of Python stdlib, other than the typing-extensions
module for Python < 3.10.
There's a few choices available to specify alias field mappings, but in the below example I chose two options to illustrate:
json_field
, which can be considered an alias to dataclasses.field
json_key_to_field
mapping that can be specified in the Meta config for a dataclassfrom dataclasses import dataclass
from typing import List
from dataclass_wizard import JSONWizard, json_field
@dataclass
class Foo:
# pass all=True, so reverse mapping (field -> JSON) is also added
foo_name: str = json_field('FOO NAME', all=True)
@dataclass
class Bar:
# default key transform is `camelCase`, so alias is not needed here
bar_name: str
@dataclass
class Baz(JSONWizard):
class _(JSONWizard.Meta):
json_key_to_field = {
# Pass '__all__', so reverse mapping (field -> JSON) is also added
'__all__': True,
'B A Z': 'baz_name',
'BAZ FOO': 'baz_foo',
'BAZ BAR': 'baz_bar'
}
baz_name: str
baz_foo: List[Foo]
baz_bar: List[Bar]
# encode
baz_e = Baz("name", [Foo('one'), Foo('two')], [Bar('first')])
json_baz_d = baz_e.to_dict()
print(json_baz_d)
# {'B A Z': 'name', 'BAZ FOO': [{'FOO NAME': 'one'}, {'FOO NAME': 'two'}], 'BAZ BAR': [{'barName': 'first'}]}
# decode
baz_d = Baz.from_dict(json_baz_d) # back to class instance
print(repr(baz_d))
# > Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
# True
assert baz_e == baz_d
NB: I noticed one obvious thing that I wanted to point out, as it seemed to not result in expected behavior. In the question above, you appear to be instantiating a Baz
instance as follows:
baz_e = Baz("name", [{"foo_name": "one"}, {"foo_name": "two"}], [{"bar_name": "first"}])
However, note that the value for the baz_foo
field, in this case, is a list of Python dict
objects rather than a list of Foo
instances. To fix this, in above solution I've changed the {"foo_name": "one"}
for example to Foo('one')
.
It is possible that the solution with dataclasses-json
makes the code more readable and cleaner.
pip install dataclasses-json
This library provides a simple API for encoding and decoding dataclasses to and from JSON.
import json
from typing import List
from dataclasses import dataclass, asdict, field
from dataclasses_json import config, dataclass_json
@dataclass_json
@dataclass
class Foo:
foo_name: str = field(metadata=config(field_name="FOO NAME")) # foo_name -> FOO NAME
@dataclass_json
@dataclass
class Bar:
bar_name: str = field(metadata=config(field_name="barName")) # bar_name -> barName
@dataclass_json
@dataclass
class Baz:
baz_name: str = field(metadata=config(field_name="B A Z")) # baz_name -> B A Z
baz_foo: List[Foo] = field(metadata=config(field_name="BAZ FOO")) # baz_foo -> BAZ FOO
baz_bar: List[Bar] = field(metadata=config(field_name="BAZ BAR")) # baz_bar -> BAZ BAR
# encode
baz_e = Baz("name", [{"FOO NAME": "one"}, {"FOO NAME": "two"}], [{"barName": "first"}])
print(baz_e.to_dict())
# {'B A Z': 'name', 'BAZ FOO': [{'FOO NAME': 'one'}, {'FOO NAME': 'two'}], 'BAZ BAR': [{'barName': 'first'}]}
# decode
json_baz_d = {
"B A Z": "name",
"BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}],
"BAZ BAR":[{"barName": "first"}]
}
baz_d = Baz.from_dict(json_baz_d) # back to class instance
print(baz_d)
# Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
# Mini test
test_from_to = Baz.from_json(baz_e.to_json())
print(test_from_to)
# Baz(baz_name='name', baz_foo=[Foo(foo_name='one'), Foo(foo_name='two')], baz_bar=[Bar(bar_name='first')])
test_to_from = Baz.to_json(test_from_to)
print(test_to_from)
# {"B A Z": "name", "BAZ FOO": [{"FOO NAME": "one"}, {"FOO NAME": "two"}], "BAZ BAR": [{"barName": "first"}]}
JSON letter case by convention is camelCase, in Python members are by convention snake_case.
You can configure it to encode/decode from other casing schemes at both the class level and the field level.
from dataclasses import dataclass, field
from dataclasses_json import LetterCase, config, dataclass_json
# changing casing at the class level
@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Foo:
foo_bar: str
foo_baz: str
f = Foo('one', 'two').to_json()
print(f)
# {"fooBar": "one", "fooBaz": "two"}
# at the field level
@dataclass_json
@dataclass
class Foo:
foo_bar: str = field(metadata=config(letter_case=LetterCase.CAMEL))
foo_baz: str
f = Foo('one', 'two').to_json()
print(f)
# {"fooBar": "one", "foo_baz": "two"}
ff = Foo.from_json(f)
print(ff)
# Foo(foo_bar='one', foo_baz='two')
Note:
ImportError: cannot import name '_TypedDictMeta' from 'typing_extensions'
You probably have an older version of typing-extensions, it is necessary to update it to the latest one.
pip install typing-extensions -U
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With