Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override names of dataclasses attributes in Python?

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?

like image 978
Milovan Tomašević Avatar asked Dec 16 '21 12:12

Milovan Tomašević


People also ask

What is __ Post_init __ Python?

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 .

Can DataClasses have methods Python?

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.

What is @dataclass in Python?

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.

How does Dataclass work in Python?

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.

What is a dataclass in Python?

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.

How do you list fields in a data class in Python?

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.

What are the parameters to dataclass () in Python?

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.

What is class_or_instance in Python?

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.


2 Answers

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
  • 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').

like image 107
rv.kvetch Avatar answered Oct 17 '22 18:10

rv.kvetch


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 
like image 1
Milovan Tomašević Avatar answered Oct 17 '22 17:10

Milovan Tomašević