Je souhaite utiliser Python pour convertir des données JSON en objet Python.
Je reçois des objets de données JSON de l'API Facebook, que je veux stocker dans ma base de données.
Ma vue actuelle dans Django (Python) (request.POST
contient le JSON):
response = request.POST
user = FbApiUser(user_id = response['id'])
user.name = response['name']
user.username = response['username']
user.save()
Cela fonctionne bien, mais comment gérer des objets de données JSON complexes?
Ne serait-il pas préférable de convertir en quelque sorte cet objet JSON en objet Python pour une utilisation facile?
Vous pouvez le faire sur une seule ligne, en utilisant namedtuple
et object_hook
:
import json
from collections import namedtuple
data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'
# Parse JSON into an object with attributes corresponding to dict keys.
x = json.loads(data, object_hook=lambda d: namedtuple('X', d.keys())(*d.values()))
print x.name, x.hometown.name, x.hometown.id
ou, pour le réutiliser facilement:
def _json_object_hook(d): return namedtuple('X', d.keys())(*d.values())
def json2obj(data): return json.loads(data, object_hook=_json_object_hook)
x = json2obj(data)
Si vous voulez qu'il gère les clés qui ne sont pas de bons noms d'attribut, consultez le paramètre namedtuple
' rename
.
Consultez la section intitulée Spécialisation du décodage des objets JSON dans la json
documentation du module . Vous pouvez l'utiliser pour décoder un objet JSON en un type Python spécifique.
Voici un exemple:
class User(object):
def __init__(self, name, username):
self.name = name
self.username = username
import json
def object_decoder(obj):
if '__type__' in obj and obj['__type__'] == 'User':
return User(obj['name'], obj['username'])
return obj
json.loads('{"__type__": "User", "name": "John Smith", "username": "jsmith"}',
object_hook=object_decoder)
print type(User) # -> <type 'type'>
Mettre à jour
Si vous souhaitez accéder aux données d'un dictionnaire via le module json, procédez comme suit:
user = json.loads('{"__type__": "User", "name": "John Smith", "username": "jsmith"}')
print user['name']
print user['username']
Juste comme un dictionnaire ordinaire.
Ce n'est pas du code golf, mais voici mon astuce la plus courte: utiliser types.SimpleNamespace
en tant que conteneur d'objets JSON.
Par rapport à la solution namedtuple
principale, elle est:
rename
et probablement la même limitation pour les clés qui ne sont pas des identifiants valides (utilise setattr
sous les couvertures)Exemple:
from __future__ import print_function
import json
try:
from types import SimpleNamespace as Namespace
except ImportError:
# Python 2.x fallback
from argparse import Namespace
data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'
x = json.loads(data, object_hook=lambda d: Namespace(**d))
print (x.name, x.hometown.name, x.hometown.id)
Vous pouvez essayer ceci:
class User(object):
def __init__(self, name, username, *args, **kwargs):
self.name = name
self.username = username
import json
j = json.loads(your_json)
u = User(**j)
Créez simplement un nouvel objet et transmettez les paramètres sous forme de carte.
Voici une alternative rapide et sale json pickle
import json
class User:
def __init__(self, name, username):
self.name = name
self.username = username
def to_json(self):
return json.dumps(self.__dict__)
@classmethod
def from_json(cls, json_str):
json_dict = json.loads(json_str)
return cls(**json_dict)
# example usage
User("tbrown", "Tom Brown").to_json()
User.from_json(User("tbrown", "Tom Brown").to_json()).to_json()
Pour les objets complexes, vous pouvez utiliser JSON Pickle
Bibliothèque Python pour la sérialisation de tout graphe d'objet arbitraire en JSON . Il peut prendre presque n'importe quel objet Python et le transformer en JSON . De plus, il peut reconstituer l'objet dans Python.
J'ai écrit un petit cadre de (dé) sérialisation appelé any2any qui permet d'effectuer des transformations complexes entre deux types Python.
Dans votre cas, je suppose que vous souhaitez transformer un dictionnaire (obtenu avec json.loads
) en un objet complexe response.education ; response.name
, avec une structure imbriquée response.education.id
, etc., donc c'est exactement le but de ce cadre. La documentation n'est pas encore excellente, mais en utilisant any2any.simple.MappingToObject
, vous devriez pouvoir le faire très facilement. S'il vous plaît demander si vous avez besoin d'aide.
Si vous utilisez Python 3.5+, vous pouvez utiliser jsons
pour sérialiser et désérialiser les anciens objets Python:
import jsons
response = request.POST
# You'll need your class attributes to match your dict keys, so in your case do:
response['id'] = response.pop('user_id')
# Then you can load that dict into your class:
user = jsons.load(response, FbApiUser)
user.save()
Vous pouvez également faire en sorte que FbApiUser
hérite de jsons.JsonSerializable
pour plus d'élégance:
user = FbApiUser.from_json(response)
Ces exemples fonctionneront si votre classe comprend des types par défaut Python, tels que des chaînes, des entiers, des listes, des dates/heures, etc. La jsons
lib nécessitera toutefois des indicateurs de type pour les types personnalisés.
Modification un peu de la réponse @DS, à charger depuis un fichier:
def _json_object_hook(d): return namedtuple('X', d.keys())(*d.values())
def load_data(file_name):
with open(file_name, 'r') as file_data:
return file_data.read().replace('\n', '')
def json2obj(file_name): return json.loads(load_data(file_name), object_hook=_json_object_hook)
Une chose: cela ne peut pas charger les articles avec des nombres devant. Comme ça:
{
"1_first_item": {
"A": "1",
"B": "2"
}
}
Parce que "1_first_item" n’est pas un nom de champ python valide.
Python3.x
La meilleure approche que je pouvais atteindre avec ma connaissance était la suivante.
Notez que ce code traite également set ().
Cette approche est générique nécessitant simplement l’extension de la classe (dans le deuxième exemple).
Notez que je ne le fais que pour les fichiers, mais il est facile de modifier le comportement à votre goût.
Cependant, ceci est un CoDec.
Avec un peu plus de travail, vous pouvez construire votre classe de différentes manières ... Je suppose qu'un constructeur par défaut l'illustre, puis je mets à jour la classe dict.
import json
import collections
class JsonClassSerializable(json.JSONEncoder):
REGISTERED_CLASS = {}
def register(ctype):
JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype
def default(self, obj):
if isinstance(obj, collections.Set):
return dict(_set_object=list(obj))
if isinstance(obj, JsonClassSerializable):
jclass = {}
jclass["name"] = type(obj).__name__
jclass["dict"] = obj.__dict__
return dict(_class_object=jclass)
else:
return json.JSONEncoder.default(self, obj)
def json_to_class(self, dct):
if '_set_object' in dct:
return set(dct['_set_object'])
Elif '_class_object' in dct:
cclass = dct['_class_object']
cclass_name = cclass["name"]
if cclass_name not in self.REGISTERED_CLASS:
raise RuntimeError(
"Class {} not registered in JSON Parser"
.format(cclass["name"])
)
instance = self.REGISTERED_CLASS[cclass_name]()
instance.__dict__ = cclass["dict"]
return instance
return dct
def encode_(self, file):
with open(file, 'w') as outfile:
json.dump(
self.__dict__, outfile,
cls=JsonClassSerializable,
indent=4,
sort_keys=True
)
def decode_(self, file):
try:
with open(file, 'r') as infile:
self.__dict__ = json.load(
infile,
object_hook=self.json_to_class
)
except FileNotFoundError:
print("Persistence load failed "
"'{}' do not exists".format(file)
)
class C(JsonClassSerializable):
def __init__(self):
self.mill = "s"
JsonClassSerializable.register(C)
class B(JsonClassSerializable):
def __init__(self):
self.a = 1230
self.c = C()
JsonClassSerializable.register(B)
class A(JsonClassSerializable):
def __init__(self):
self.a = 1
self.b = {1, 2}
self.c = B()
JsonClassSerializable.register(A)
A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)
Modifier
Après quelques recherches supplémentaires, j'ai trouvé un moyen de généraliser sans recourir à l'appel de la méthode de registre DE LA SUPERCLASSE, à l'aide d'un metaclass
import json
import collections
REGISTERED_CLASS = {}
class MetaSerializable(type):
def __call__(cls, *args, **kwargs):
if cls.__not in REGISTERED_CLASS:
REGISTERED_CLASS[cls.__name__] = cls
return super(MetaSerializable, cls).__call__(*args, **kwargs)
class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):
def default(self, obj):
if isinstance(obj, collections.Set):
return dict(_set_object=list(obj))
if isinstance(obj, JsonClassSerializable):
jclass = {}
jclass["name"] = type(obj).__name__
jclass["dict"] = obj.__dict__
return dict(_class_object=jclass)
else:
return json.JSONEncoder.default(self, obj)
def json_to_class(self, dct):
if '_set_object' in dct:
return set(dct['_set_object'])
Elif '_class_object' in dct:
cclass = dct['_class_object']
cclass_name = cclass["name"]
if cclass_name not in REGISTERED_CLASS:
raise RuntimeError(
"Class {} not registered in JSON Parser"
.format(cclass["name"])
)
instance = REGISTERED_CLASS[cclass_name]()
instance.__dict__ = cclass["dict"]
return instance
return dct
def encode_(self, file):
with open(file, 'w') as outfile:
json.dump(
self.__dict__, outfile,
cls=JsonClassSerializable,
indent=4,
sort_keys=True
)
def decode_(self, file):
try:
with open(file, 'r') as infile:
self.__dict__ = json.load(
infile,
object_hook=self.json_to_class
)
except FileNotFoundError:
print("Persistence load failed "
"'{}' do not exists".format(file)
)
class C(JsonClassSerializable):
def __init__(self):
self.mill = "s"
class B(JsonClassSerializable):
def __init__(self):
self.a = 1230
self.c = C()
class A(JsonClassSerializable):
def __init__(self):
self.a = 1
self.b = {1, 2}
self.c = B()
A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s
Améliorer la très bonne réponse du lovasoa.
Si vous utilisez python 3.6+, vous pouvez utiliser:pip install Marshmallow-enum
etpip install Marshmallow-dataclass
C'est simple et sûr.
Vous pouvez transformer votre classe en string-json et vice-versa:
De l'objet à la chaîne Json:
from Marshmallow_dataclass import dataclass
user = User("Danilo","50","RedBull",15,OrderStatus.CREATED)
user_json = User.Schema().dumps(user)
user_json_str = user_json.data
De la chaîne Json à l'objet:
json_str = '{"name":"Danilo", "orderId":"50", "productName":"RedBull", "quantity":15, "status":"Created"}'
user, err = User.Schema().loads(json_str)
print(user,flush=True)
Définitions de classe:
class OrderStatus(Enum):
CREATED = 'Created'
PENDING = 'Pending'
CONFIRMED = 'Confirmed'
FAILED = 'Failed'
@dataclass
class User:
def __init__(self, name, orderId, productName, quantity, status):
self.name = name
self.orderId = orderId
self.productName = productName
self.quantity = quantity
self.status = status
name: str
orderId: str
productName: str
quantity: int
status: OrderStatus
En cherchant une solution, je suis tombé par hasard sur cet article de blog: https://blog.mosthege.net/2016/11/12/json-deserialization-of-nested-objects/
Il utilise la même technique que celle indiquée dans les réponses précédentes, mais avec un usage de décorateurs. Une autre chose que j’ai trouvée utile est le fait qu’il renvoie un objet typé à la fin de la désérialisation
class JsonConvert(object):
class_mappings = {}
@classmethod
def class_mapper(cls, d):
for keys, cls in clsself.mappings.items():
if keys.issuperset(d.keys()): # are all required arguments present?
return cls(**d)
else:
# Raise exception instead of silently returning None
raise ValueError('Unable to find a matching class for object: {!s}'.format(d))
@classmethod
def complex_handler(cls, Obj):
if hasattr(Obj, '__dict__'):
return Obj.__dict__
else:
raise TypeError('Object of type %s with value of %s is not JSON serializable' % (type(Obj), repr(Obj)))
@classmethod
def register(cls, claz):
clsself.mappings[frozenset(Tuple([attr for attr,val in cls().__dict__.items()]))] = cls
return cls
@classmethod
def to_json(cls, obj):
return json.dumps(obj.__dict__, default=cls.complex_handler, indent=4)
@classmethod
def from_json(cls, json_str):
return json.loads(json_str, object_hook=cls.class_mapper)
Usage:
@JsonConvert.register
class Employee(object):
def __init__(self, Name:int=None, Age:int=None):
self.Name = Name
self.Age = Age
return
@JsonConvert.register
class Company(object):
def __init__(self, Name:str="", Employees:[Employee]=None):
self.Name = Name
self.Employees = [] if Employees is None else Employees
return
company = Company("Contonso")
company.Employees.append(Employee("Werner", 38))
company.Employees.append(Employee("Mary"))
as_json = JsonConvert.to_json(company)
from_json = JsonConvert.from_json(as_json)
as_json_from_json = JsonConvert.to_json(from_json)
assert(as_json_from_json == as_json)
print(as_json_from_json)
Puisque personne n’a fourni une réponse aussi semblable que la mienne, je vais la poster ici.
Il s'agit d'une classe robuste qui peut facilement convertir entre json str
et dict
que j'ai copié de ma réponse à une autre question :
import json
class PyJSON(object):
def __init__(self, d):
if type(d) is str:
d = json.loads(d)
self.from_dict(d)
def from_dict(self, d):
self.__dict__ = {}
for key, value in d.items():
if type(value) is dict:
value = PyJSON(value)
self.__dict__[key] = value
def to_dict(self):
d = {}
for key, value in self.__dict__.items():
if type(value) is PyJSON:
value = value.to_dict()
d[key] = value
return d
def __repr__(self):
return str(self.to_dict())
def __setitem__(self, key, value):
self.__dict__[key] = value
def __getitem__(self, key):
return self.__dict__[key]
json_str = """... json string ..."""
py_json = PyJSON(json_str)
En développant un peu la réponse de DS, si vous avez besoin que l’objet soit mutable (ce que namedtuple n’est pas), vous pouvez utiliser la bibliothèque recordclass au lieu de namedtuple:
import json
from recordclass import recordclass
data = '{"name": "John Smith", "hometown": {"name": "New York", "id": 123}}'
# Parse into a mutable object
x = json.loads(data, object_hook=lambda d: recordclass('X', d.keys())(*d.values()))
L'objet modifié peut ensuite être reconverti très facilement en json en utilisant simplejson :
x.name = "John Doe"
new_json = simplejson.dumps(x)
Si vous utilisez python 3.6+, vous pouvez utiliser Marshmallow-dataclass . Contrairement à toutes les solutions énumérées ci-dessus, il est à la fois simple et sûr:
from Marshmallow_dataclass import dataclass
@dataclass
class User:
name: str
user, err = User.Schema().load({"name": "Ramirez"})