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, and if so, which would be the most elegant solution?
CodePudding user response:
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 todataclasses.field
- a
json_key_to_field
mapping that can be specified in the Meta config for a dataclass
from 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')
.
CodePudding user response:
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"}]}
Encode or decode from camelCase (or kebab-case)?
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:
- In case of error:
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