Skip to content

Instantly share code, notes, and snippets.

@gatopeich
Created February 19, 2019 15:08
Show Gist options
  • Save gatopeich/1efd3e1e4269e1e98fae9983bb914f22 to your computer and use it in GitHub Desktop.
Save gatopeich/1efd3e1e4269e1e98fae9983bb914f22 to your computer and use it in GitHub Desktop.
Python 3.7 dataclass to/from dict/json
from dataclasses import dataclass, fields as datafields
from ujson import dumps, loads
# Note: ujson seamlessly serializes dataclasses, unlike stdlib's json
@dataclass
class Point:
x: float
y: float
# Shallow dataclass can be rebuilt from dict/json:
point = Point(1,2)
assert point == Point(**loads(dumps(point)))
# However, deep dataclass's fields won't be rebuilt from the json dict:
@dataclass
class Line:
a: Point
b: Point
line = Line(Point(1,2), Point(3,4))
assert line != Line(**loads(dumps(line)))
print(line, '\n !=', Line(**loads(dumps(line))))
# Line(a=Point(x=1, y=2), b=Point(x=3, y=4))
# != Line(a={'x': 1, 'y': 2}, b={'x': 3, 'y': 4})
# But we can simply reconstruct recursively:
def dataclass_from_dict(klass, dikt):
try:
fieldtypes = {f.name:f.type for f in datafields(klass)}
return klass(**{f:dataclass_from_dict(fieldtypes[f],dikt[f]) for f in dikt})
except:
return dikt
line_from_dict = dataclass_from_dict(Line,loads(dumps(line)))
assert line == line_from_dict
@hleb-albau
Copy link

note: will not work for fields with type of list

@Higgcz
Copy link

Higgcz commented Mar 10, 2021

I think it should be easy to add support for lists and tuples:

def dataclass_from_dict(klass, dikt):
    try:
        fieldtypes = klass.__annotations__  # this can be also simplified I believe
        return klass(**{f: dataclass_from_dict(fieldtypes[f], dikt[f]) for f in dikt})
    except Exception:
        if isinstance(dikt, (tuple, list)):
            return [dataclass_from_dict(klass.__args__[0], f) for f in dikt]
        return dikt

This assumes that your type in dataclass is defined using typing module.

Example:

@dataclass
class Lines:
    lines: List[Line]

lines = Lines(lines=[
     Line(Point(1, 2), Point(3, 4)),
     Line(Point(3, 4), Point(5, 6))
])

dataclass_from_dict(Lines, asdict(lines)))

Note: I think using asdict is fine, haven't noticed any problem so far.

@EnlNovius
Copy link

I think your solution is good. I would just change the type of the exception (and by the way correct the d by dikt in the exception).

def dataclass_from_dict(klass, dikt):
    try:
        fieldtypes = klass.__annotations__
        return klass(**{f: dataclass_from_dict(fieldtypes[f], dikt[f]) for f in dikt})
    except AttributeError:
        if isinstance(dikt, (tuple, list)):
            return [dataclass_from_dict(klass.__args__[0], f) for f in dikt]
        return dikt

@Higgcz
Copy link

Higgcz commented Apr 7, 2021

@EnlNovius Thanks, I've missed the d. Also using AttributeError makes probably more sense, cannot think of any potential problem.

@nflatrea
Copy link

Using annotations you remove all inherited fields

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment