I have a JSON data structure. Every object has a field called "type"
.
json_data_str = """
{
"type" : "Game",
"levels" : [
{
"type": "Level",
"map" : {
"type" : "SquareRoom",
"name" : "Level 1",
"width" : 100,
"height" : 100
},
"waves" : [
{
"type" : "Wave",
"enemies" : [
{
"type" : "Wizard",
"name" : "Gandalf"
},
{
"type" : "Archer",
"name" : "Legolass"
}
]
}
]
}
]
}
"""
And I want to convert this into an object tree composed of the following classes
from dataclasses import dataclass
from typing import List
@dataclass
class GameObject:
...
@dataclass
class Character(GameObject):
name: str
@dataclass
class Wave(GameObject):
enemies: List[Character]
@dataclass
class Wizard(Character):
...
@dataclass
class Archer(Character):
...
@dataclass
class Map(GameObject):
name: str
@dataclass
class SquareRoom(Map):
width: int
height: int
@dataclass
class Level(GameObject):
waves: List[Wave]
map: Map
@dataclass
class Game(GameObject):
levels: List[Level]
I can unpack a simple json object into a dataclass quite easily using the **
operator: eg
json_data_str = """
{
"type" : "Person"
"name" : "Bob"
"age" : 29
}
"""
class GameObject(ABC):
...
@dataclass
class Person(GameObject):
name: str
age: int
game_object_registry: Dict[str, Type[GameObject]] = {}
game_object_registry['Person'] = Person
json_obj = json.loads(json_data_str)
obj_type = json_obj['type']
del json_obj['type']
ObjType = game_object_registry[obj_type]
ObjType(**json_obj)
But how can I extend this to work with nested objects?
I want it to create this data class instance:
game = Game(levels=[Level(map=SquareRoom(name="Level 1", width=100, height=100), waves=[Wave([Wizard(name="Gandalf"), Archer(name="Legolass")])])])
Here is my best attempt. It doesn't really make sense, but it might be a starting point. I realise this logic doesn't make sense, but I cannot come up with a function that does make sense.
def json_to_game_object(json_obj: Any, game_object_registry: Dict[str, Type[GameObject]]) -> Any:
if type(json_obj) is dict:
obj_type: str = json_obj['type']
del json_obj['type']
ObjType = game_object_registry[obj_type]
for key, value in json_obj.items():
logging.debug(f'Parsing feild "{key}:{value}"')
json_to_game_object(value, game_object_registry)
if type(value) is dict:
logging.debug(f'Creating object of type {ObjType} with args {value}')
return ObjType(**value)
elif type(json_obj) is list:
logging.debug(f'Parsing JSON List')
for elem in json_obj:
logging.debug(f'Parsing list element "{json_obj.index(elem)}"')
json_to_game_object(elem, game_object_registry)
else:
logging.debug(f'Parsing value')
Here is an example of how do this export in an object-oriented way. In my personal opinion, converting this to pure dataclass objects brings you no benefit over just keeping everything in a dictionary. Here, each object can have its own behavior.
(I've now modified this to start adding repr handlers, so you can print the whole tree at once.)
json_data_str = """
{
"type" : "Game",
"levels" : [
{
"type": "Level",
"map" : {
"type" : "SquareRoom",
"name" : "Level 1",
"width" : 100,
"height" : 100
},
"waves" : [
{
"type" : "Wave",
"enemies" : [
{
"type" : "Wizard",
"name" : "Gandalf"
},
{
"type" : "Archer",
"name" : "Legolass"
}
]
}
]
}
]
}
"""
import json
class GameObject():
pass
class Game(GameObject):
def __init__(self, obj):
self.levels = [Level(k) for k in obj['levels']]
def __repr__(self):
s = f"<Game contains {len(self.levels)} levels:>\n"
s += '\n'.join(repr(l) for l in self.levels)
return s
class Character(GameObject):
def __init__(self,obj):
self.name = obj['name']
class Wave(GameObject):
def __init__(self, obj):
self.enemies = [game_object_registry[e['type']](e) for e in obj['enemies']]
def __repr__(self):
return f'<Wave contains {len(self.enemies)} enemies'
class Wizard(Character):
def __init__(self,obj):
super().__init__(obj)
class Archer(Character):
def __init__(self,obj):
super().__init__(obj)
class Map(GameObject):
pass
class SquareRoom(Map):
def __init__(self,obj):
self.name = obj['name']
self.widdth = obj['width']
self.height = obj['height']
class Level(GameObject):
def __init__(self, obj):
self.waves = [Wave(e) for e in obj['waves']]
self.map = game_object_registry[obj['map']['type']](obj['map'])
def __repr__(self):
s = f'<Level contains {len(self.waves)} waves>\n'
s += '\n'.join(repr(w) for w in self.waves)
return s
game_object_registry = {
'Game': Game,
'Wave': Wave,
'Level': Level,
'SquareRoom': SquareRoom,
'Archer': Archer,
'Map': Map,
'Wizard': Wizard
}
json_obj = json.loads(json_data_str)
g = Game(json_obj)
print(g)
print(g.levels[0].waves[0].enemies[0].name)
Assuming you have control over the JSON / dict structure. You can use a framework like dacite .
It will let you map the data into your dataclasses.
Example (taken from dacite github) below:
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a: A
data = {
'a': {
'x': 'test',
'y': 1,
}
}
result = from_dict(data_class=B, data=data)
assert result == B(a=A(x='test', y=1))
As an alternative, you can also use the dataclass-wizard library for this. This should support dataclasses as Union
types as of a recent version, however note that the tag field is not configurable as of now, so in below I've changed the type
field that appears in the JSON object; I've also removed this field entirely in cases where it was not really needed -- note that you'd only need a tag field when you have a dataclass field that maps to one or more dataclass types.
The below example should work for Python 3.7+ with the included __future__
import.
from __future__ import annotations
from dataclasses import dataclass
from dataclass_wizard import JSONWizard
@dataclass
class GameObject:
...
@dataclass
class Character(GameObject):
name: str
@dataclass
class Wizard(Character, JSONWizard):
class _(JSONWizard.Meta):
tag = 'Wizard'
...
@dataclass
class Archer(Character, JSONWizard):
class _(JSONWizard.Meta):
tag = 'Archer'
...
@dataclass
class Game(GameObject, JSONWizard):
levels: list[Level]
@dataclass
class Level(GameObject):
waves: list[Wave]
# TODO: define other map classes
map: SquareRoom | Map
@dataclass
class Map(GameObject):
name: str
@dataclass
class SquareRoom(Map, JSONWizard):
class _(JSONWizard.Meta):
tag = 'SquareRoom'
width: int
height: int
@dataclass
class Wave(GameObject):
enemies: list[Wizard | Archer]
def main():
json_data_str = """
{
"levels": [
{
"map": {
"__tag__": "SquareRoom",
"name": "Level 1",
"width": 100,
"height": 100
},
"waves": [
{
"enemies": [
{
"__tag__": "Wizard",
"name": "Gandalf"
},
{
"__tag__": "Archer",
"name": "Legolass"
}
]
}
]
}
]
}
"""
game = Game.from_json(json_data_str)
print(repr(game))
if __name__ == '__main__':
main()
Output:
Game(levels=[Level(waves=[Wave(enemies=[Wizard(name='Gandalf'), Archer(name='Legolass')])], map=SquareRoom(name='Level 1', width=100, height=100))])
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.