web-dev-qa-db-fra.com

Comment rendre une classe JSON sérialisable

Comment rendre une classe Python sérialisable? 

Un cours simple:

class FileItem:
    def __init__(self, fname):
        self.fname = fname

Que dois-je faire pour pouvoir obtenir les résultats suivants:

json.dumps()

Sans erreur (FileItem instance at ... is not JSON serializable)

579
Sergey

Avez-vous une idée de la sortie attendue? Par exemple ça fera l'affraire?

>>> f  = FileItem("/foo/bar")
>>> magic(f)
'{"fname": "/foo/bar"}'

Dans ce cas, vous pouvez simplement appeler json.dumps(f.__dict__)

Si vous souhaitez une sortie plus personnalisée, vous devrez sous-classe JSONEncoder et implémenter votre propre sérialisation personnalisée. 

Pour un exemple trivial, voir ci-dessous.

>>> from json import JSONEncoder
>>> class MyEncoder(JSONEncoder):
        def default(self, o):
            return o.__dict__    

>>> MyEncoder().encode(f)
'{"fname": "/foo/bar"}'

Ensuite, vous transmettez cette classe à la méthode json.dumps() sous la forme cls kwarg:

json.dumps(cls=MyEncoder)

Si vous souhaitez également décoder, vous devrez fournir un object_hook personnalisé à la classe JSONDecoder . Par exemple.

>>> def from_json(json_object):
        if 'fname' in json_object:
            return FileItem(json_object['fname'])
>>> f = JSONDecoder(object_hook = from_json).decode('{"fname": "/foo/bar"}')
>>> f
<__main__.FileItem object at 0x9337fac>
>>> 
437
Manoj Govindan

Voici une solution simple pour une fonctionnalité simple:

.toJSON() Méthode

Au lieu d'une classe sérialisable JSON, implémentez une méthode de sérialiseur:

import json

class Object:
    def toJSON(self):
        return json.dumps(self, default=lambda o: o.__dict__, 
            sort_keys=True, indent=4)

Donc, vous appelez juste pour sérialiser:

me = Object()
me.name = "Onur"
me.age = 35
me.dog = Object()
me.dog.name = "Apollo"

print(me.toJSON())

affichera:

{
    "age": 35,
    "dog": {
        "name": "Apollo"
    },
    "name": "Onur"
}
490
Onur Yıldırım

Pour les classes plus complexes, vous pouvez utiliser l'outil jsonpickle :

jsonpickle est une bibliothèque Python pour la sérialisation et la désérialisation d'objets Python complexes vers et depuis JSON.

Les bibliothèques Python standard pour coder Python en JSON, telles que json, simplejson et demjson de stdlib, ne peuvent gérer que les primitives Python ayant un équivalent JSON direct (p. Ex. Dicts, listes, chaînes, entiers, etc.). jsonpickle repose sur ces bibliothèques et permet la sérialisation de structures de données plus complexes en JSON. jsonpickle est hautement configurable et extensible, permettant à l'utilisateur de choisir le backend JSON et d'ajouter des backends supplémentaires.

(lien vers jsonpickle sur PyPi) }

130
gecco

La plupart des réponses impliquent de changer l'appel en json.dumps () , ce qui n'est pas toujours possible ou souhaitable (cela peut arriver dans un composant cadre par exemple).

Si vous voulez pouvoir appeler json.dumps (obj) tel quel, une solution simple consiste à hériter de dict :

class FileItem(dict):
    def __init__(self, fname):
        dict.__init__(self, fname=fname)

f = FileItem('tasks.txt')
json.dumps(f)  #No need to change anything here

Cela fonctionne si votre classe est juste une représentation de base de données. Pour les choses plus délicates, vous pouvez toujours définir les clés de manière explicite.

54
andyhasit

Une autre option consiste à intégrer le dumping JSON dans sa propre classe:

import json

class FileItem:
    def __init__(self, fname):
        self.fname = fname

    def __repr__(self):
        return json.dumps(self.__dict__)

Ou encore mieux, sous-classez la classe FileItem d'une classe JsonSerializable:

import json

class JsonSerializable(object):
    def toJson(self):
        return json.dumps(self.__dict__)

    def __repr__(self):
        return self.toJson()


class FileItem(JsonSerializable):
    def __init__(self, fname):
        self.fname = fname

Essai:

>>> f = FileItem('/foo/bar')
>>> f.toJson()
'{"fname": "/foo/bar"}'
>>> f
'{"fname": "/foo/bar"}'
>>> str(f) # string coercion
'{"fname": "/foo/bar"}'
34
Paulo Freitas

J'aime bien la réponse de Onur , mais je voudrais développer une méthode optionnelle toJSON() pour que les objets se sérialisent:

def dumper(obj):
    try:
        return obj.toJSON()
    except:
        return obj.__dict__
print json.dumps(some_big_object, default=dumper, indent=2)
29
Jason S

L’autre jour, j’ai rencontré ce problème et j’ai implémenté une version plus générale d’un objet Encoder for Python qui peut gérer les objets imbriqués et les champs hérités :

import json
import inspect

class ObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, "to_json"):
            return self.default(obj.to_json())
        Elif hasattr(obj, "__dict__"):
            d = dict(
                (key, value)
                for key, value in inspect.getmembers(obj)
                if not key.startswith("__")
                and not inspect.isabstract(value)
                and not inspect.isbuiltin(value)
                and not inspect.isfunction(value)
                and not inspect.isgenerator(value)
                and not inspect.isgeneratorfunction(value)
                and not inspect.ismethod(value)
                and not inspect.ismethoddescriptor(value)
                and not inspect.isroutine(value)
            )
            return self.default(d)
        return obj

Exemple:

class C(object):
    c = "NO"
    def to_json(self):
        return {"c": "YES"}

class B(object):
    b = "B"
    i = "I"
    def __init__(self, y):
        self.y = y

    def f(self):
        print "f"

class A(B):
    a = "A"
    def __init__(self):
        self.b = [{"ab": B("y")}]
        self.c = C()

print json.dumps(A(), cls=ObjectEncoder, indent=2, sort_keys=True)

Résultat:

{
  "a": "A", 
  "b": [
    {
      "ab": {
        "b": "B", 
        "i": "I", 
        "y": "y"
      }
    }
  ], 
  "c": {
    "c": "YES"
  }, 
  "i": "I"
}
23
tobigue

Ajoutez simplement la méthode to_json à votre classe comme ceci:

def to_json(self):
  return self.message # or how you want it to be serialized

Et ajoutez ce code (de cette réponse ), quelque part au sommet de tout:

from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder().default
JSONEncoder.default = _default

Cela va corriger le module json lorsqu’il est importé, donc JSONEncoder.default () recherche automatiquement une méthode spéciale "to_json ()" Et l’utilise pour coder l’objet, le cas échéant.

Tout comme Onur l'a dit, mais cette fois-ci, vous n'avez pas à mettre à jour tous les json.dumps() de votre projet.

17
Fancy John
import simplejson

class User(object):
    def __init__(self, name, mail):
        self.name = name
        self.mail = mail

    def _asdict(self):
        return self.__dict__

print(simplejson.dumps(User('alice', '[email protected]')))

si vous utilisez la norme json, vous devez définir une fonction default

import json
def default(o):
    return o._asdict()

print(json.dumps(User('alice', '[email protected]'), default=default))
9
tryer3000

Cette classe peut faire l'affaire, elle convertit un objet en json standard.

import json


class Serializer(object):
    @staticmethod
    def serialize(object):
        return json.dumps(object, default=lambda o: o.__dict__.values()[0])

usage:

Serializer.serialize(my_object)

travaillant dans python2.7 et python3.

5
Lost Koder

json est limité en termes d’objets qu’il peut imprimer, et jsonpickle (vous aurez peut-être besoin d’un pip install jsonpickle) est limité en termes d’indéscriptibilité du texte. Si vous souhaitez inspecter le contenu d'un objet dont vous ne pouvez pas changer la classe, je ne pouvais toujours pas trouver un moyen plus simple que:

 import json
 import jsonpickle
 ...
 print  json.dumps(json.loads(jsonpickle.encode(object)), indent=2)

Notez qu'ils ne peuvent toujours pas imprimer les méthodes d'objet. 

5
ribamar

jaraco a donné une réponse plutôt soignée. J'ai eu besoin de réparer quelques petites choses, mais cela fonctionne:

Code

# Your custom class
class MyCustom(object):
    def __json__(self):
        return {
            'a': self.a,
            'b': self.b,
            '__python__': 'mymodule.submodule:MyCustom.from_json',
        }

    to_json = __json__  # supported by simplejson

    @classmethod
    def from_json(cls, json):
        obj = cls()
        obj.a = json['a']
        obj.b = json['b']
        return obj

# Dumping and loading
import simplejson

obj = MyCustom()
obj.a = 3
obj.b = 4

json = simplejson.dumps(obj, for_json=True)

# Two-step loading
obj2_dict = simplejson.loads(json)
obj2 = MyCustom.from_json(obj2_dict)

# Make sure we have the correct thing
assert isinstance(obj2, MyCustom)
assert obj2.__dict__ == obj.__dict__

Notez que nous avons besoin de deux étapes pour le chargement. Pour l'instant, la propriété __python__ n'est pas utilisée.

Est-ce que c'est commun?

En utilisant la méthode de AlJohri , je vérifie la popularité des approches:

Sérialisation (Python -> JSON):

Désérialisation (JSON -> Python):

4
Martin Thoma
import json

class Foo(object):
    def __init__(self):
        self.bar = 'baz'
        self._qux = 'flub'

    def somemethod(self):
        pass

def default(instance):
    return {k: v
            for k, v in vars(instance).items()
            if not str(k).startswith('_')}

json_foo = json.dumps(Foo(), default=default)
assert '{"bar": "baz"}' == json_foo

print(json_foo)
3
rectangletangle

jsonweb semble être la meilleure solution pour moi. Voir http://www.jsonweb.info/en/latest/

from jsonweb.encode import to_object, dumper

@to_object()
class DataModel(object):
  def __init__(self, id, value):
   self.id = id
   self.value = value

>>> data = DataModel(5, "foo")
>>> dumper(data)
'{"__type__": "DataModel", "id": 5, "value": "foo"}'
2
matthewlent

Si vous utilisez Python3.5 +, vous pouvez utiliser jsons . Il convertira votre objet (et tous ses attributs de manière récursive) en dict.

import jsons

a_dict = jsons.dump(your_object)

Ou si vous vouliez une ficelle:

a_str = jsons.dumps(your_object)

Ou si votre classe implémenté jsons.JsonSerializable:

a_dict = your_object.json
2
R H

Si cela ne vous dérange pas d'installer un paquet, vous pouvez utiliser json-tricks :

pip install json-tricks

Après cela, il vous suffit d’importer dump(s) à partir de json_tricks au lieu de json, et cela fonctionnera généralement:

from json_tricks import dumps
json_str = dumps(cls_instance, indent=4)

qui va donner

{
        "__instance_type__": [
                "module_name.test_class",
                "MyTestCls"
        ],
        "attributes": {
                "attr": "val",
                "dct_attr": {
                        "hello": 42
                }
        }
}

Et c'est fondamentalement ça!


Cela fonctionnera très bien en général. Il existe quelques exceptions, par exemple si des choses spéciales se produisent dans __new__, ou plus de métaclasse se poursuit.

Évidemment, le chargement fonctionne aussi (sinon, quel est le but):

from json_tricks import loads
json_str = loads(json_str)

Cela suppose que module_name.test_class.MyTestCls peut être importé et n'a pas été modifié de manière non compatible. Vous récupérerez une instance, pas un dictionnaire ou quelque chose du genre, et ce devrait être une copie identique à celle que vous avez sauvegardée.

Si vous voulez personnaliser la manière dont quelque chose est (ou) sérialisée, vous pouvez ajouter des méthodes spéciales à votre classe, comme ceci:

class CustomEncodeCls:
        def __init__(self):
                self.relevant = 42
                self.irrelevant = 37

        def __json_encode__(self):
                # should return primitive, serializable types like dict, list, int, string, float...
                return {'relevant': self.relevant}

        def __json_decode__(self, **attrs):
                # should initialize all properties; note that __init__ is not called implicitly
                self.relevant = attrs['relevant']
                self.irrelevant = 12

qui ne sérialise qu’une partie des paramètres d’attributs, à titre d’exemple.

Et en bonus gratuit, vous obtenez une (dé) sérialisation des tableaux numpy, des dates et heures, des cartes ordonnées, ainsi que la possibilité d'inclure des commentaires dans json.

Disclaimer: J'ai créé json_tricks , parce que j'ai eu le même problème que vous.

1
Mark

J'ai rencontré ce problème lorsque j'ai essayé de stocker le modèle de Peewee dans PostgreSQL JSONField.

Après avoir lutté pendant un moment, voici la solution générale.

La solution de ma solution passe par le code source de Python et réalise que la documentation de code (décrite ici ) explique déjà comment étendre le json.dumps existant pour prendre en charge d’autres types de données.

Supposons que votre modèle actuel contienne des champs non sérialisables en JSON et que le modèle contenant le champ JSON ressemble à ceci:

class SomeClass(Model):
    json_field = JSONField()

Définissez simplement une JSONEncoder personnalisée comme ceci:

class CustomJsonEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, SomeTypeUnsupportedByJsonDumps):
            return < whatever value you want >
        return json.JSONEncoder.default(self, obj)

    @staticmethod
    def json_dumper(obj):
        return json.dumps(obj, cls=CustomJsonEncoder)

Et ensuite, utilisez-le dans votre JSONField comme ci-dessous:

class SomeClass(Model):
    json_field = JSONField(dumps=CustomJsonEncoder.json_dumper)

La clé est la méthode default(self, obj) ci-dessus. Pour chaque réclamation ... is not JSON serializable que vous recevez de Python, ajoutez simplement du code permettant de gérer le type non sérialisable en JSON (tel que Enum ou datetime).

Par exemple, voici comment je supporte une classe héritant de Enum:

class TransactionType(Enum):
   CURRENT = 1
   STACKED = 2

   def default(self, obj):
       if isinstance(obj, TransactionType):
           return obj.value
       return json.JSONEncoder.default(self, obj)

Enfin, avec le code implémenté comme ci-dessus, vous pouvez simplement convertir n’importe quel modèle Peewee en un objet JSON-compatible comme ci-dessous:

peewee_model = WhateverPeeweeModel()
new_model = SomeClass()
new_model.json_field = model_to_dict(peewee_model)

Bien que le code ci-dessus soit (quelque peu) spécifique à Peewee, mais je pense:

  1. Cela s'applique à d'autres ORM (Django, etc.) en général
  2. De plus, si vous avez compris comment fonctionne json.dumps, cette solution fonctionne également avec Python (sans ORM) en général.

Toutes les questions, s'il vous plaît poster dans la section commentaires. Merci!

1
sivabudh

Voici mes 3 cents ...
Ceci illustre la sérialisation json explicite pour un objet python semblable à une arborescence.
Remarque: Si vous voulez réellement un code comme celui-ci, vous pouvez utiliser le twisted FilePath class.

import json, sys, os

class File:
    def __init__(self, path):
        self.path = path

    def isdir(self):
        return os.path.isdir(self.path)

    def isfile(self):
        return os.path.isfile(self.path)

    def children(self):        
        return [File(os.path.join(self.path, f)) 
                for f in os.listdir(self.path)]

    def getsize(self):        
        return os.path.getsize(self.path)

    def getModificationTime(self):
        return os.path.getmtime(self.path)

def _default(o):
    d = {}
    d['path'] = o.path
    d['isFile'] = o.isfile()
    d['isDir'] = o.isdir()
    d['mtime'] = int(o.getModificationTime())
    d['size'] = o.getsize() if o.isfile() else 0
    if o.isdir(): d['children'] = o.children()
    return d

folder = os.path.abspath('.')
json.dump(File(folder), sys.stdout, default=_default)
1
Dan Brough

Si vous êtes capable d'installer un paquet, je vous recommande d'essayer dill , qui a parfaitement fonctionné pour mon projet. Une bonne chose à propos de ce paquet est qu'il a la même interface que pickle. Ainsi, si vous avez déjà utilisé pickle dans votre projet, vous pouvez simplement le remplacer par dill et voir si le script s'exécute sans changer de code. C'est donc une solution très économique à essayer!

(Anti-divulgation complète: je ne suis aucunement affiliée à et n’ai jamais contribué au projet Dill.)

Installez le paquet:

pip install dill

Puis éditez votre code pour importer dill au lieu de pickle:

# import pickle
import dill as pickle

Exécutez votre script et voyez si cela fonctionne. (Si c'est le cas, vous voudrez peut-être nettoyer votre code afin de ne plus suivre le nom du module pickle!)

Quelques détails sur les types de données que dill peut et ne peut pas sérialiser, à partir de la page du projet :

dill peut combiner les types standard suivants:

none, type, bool, int, long, float, complex, str, unicode, Tuple, list, dict, file, buffer, builtin, anciennes et nouvelles classes de style, instances d'anciennes et de nouvelles classes de style, set, frozenset, array, fonctions, exceptions 

dill peut également mariner des types standard plus «exotiques»:

fonctions avec rendements, fonctions imbriquées, lambdas, cellule, méthode, unboundmethod, module, code, methodwrapper, dictproxy, descripteur de méthode, getsetdescriptor, memberdescriptor, wrapperdescriptor, xrange, slice, notimplemented, Ellipsis, quitter 

dill ne peut pas encore décaper ces types standard:

cadre, générateur, traceback

0
thedavidmo

Je suis venu avec ma propre solution. Utilisez cette méthode, transmettez tout document ( dict , list , ObjectId etc) à sérialiser.

def getSerializable(doc):
    # check if it's a list
    if isinstance(doc, list):
        for i, val in enumerate(doc):
            doc[i] = getSerializable(doc[i])
        return doc

    # check if it's a dict
    if isinstance(doc, dict):
        for key in doc.keys():
            doc[key] = getSerializable(doc[key])
        return doc

    # Process ObjectId
    if isinstance(doc, ObjectId):
        doc = str(doc)
        return doc

    # Use any other custom serializting stuff here...

    # For the rest of stuff
    return doc
0
Dewsworld

C'est une petite bibliothèque qui sérialise un objet avec tous ses enfants en JSON et le réanalyse:

https://github.com/Toubs/PyJSONSerialization/

0
Tobi

Je ne vois aucune mention ici de la gestion de version en série ou de backcompat, je vais donc publier ma solution que je utilise depuis un moment. J'ai probablement beaucoup plus de choses à apprendre, en particulier Java et Javascript sont probablement plus matures que moi ici, mais voilà

https://Gist.github.com/andy-d/b7878d0044a4242c0498ed6d67fd50fe

0
Fletch F Fletch

Cela a bien fonctionné pour moi: 

class JsonSerializable(object):

    def serialize(self):
        return json.dumps(self.__dict__)

    def __repr__(self):
        return self.serialize()

    @staticmethod
    def dumper(obj):
        if "serialize" in dir(obj):
            return obj.serialize()

        return obj.__dict__

et alors

class FileItem(JsonSerializable):
    ...

et

log.debug(json.dumps(<my object>, default=JsonSerializable.dumper, indent=2))
0
jmhostalet

J'ai le plus aimé la méthode de Lost Koder. J'ai rencontré des problèmes lorsque j'ai essayé de sérialiser des objets plus complexes dont les membres/méthodes ne sont pas sérialisables. Voici mon implémentation qui fonctionne sur plusieurs objets:

class Serializer(object):
    @staticmethod
    def serialize(obj):
        def check(o):
            for k, v in o.__dict__.items():
                try:
                    _ = json.dumps(v)
                    o.__dict__[k] = v
                except TypeError:
                    o.__dict__[k] = str(v)
            return o
        return json.dumps(check(obj).__dict__, indent=2)
0
Will Charlton

J'ai choisi d'utiliser des décorateurs pour résoudre le problème de sérialisation d'objet datetime ..__ Voici mon code:

#myjson.py
#Author: jmooremcc 7/16/2017

import json
from datetime import datetime, date, time, timedelta
"""
This module uses decorators to serialize date objects using json
The filename is myjson.py
In another module you simply add the following import statement:
    from myjson import json

json.dumps and json.dump will then correctly serialize datetime and date 
objects
"""

def json_serial(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, (datetime, date)):
        serial = str(obj)
        return serial
    raise TypeError ("Type %s not serializable" % type(obj))


def FixDumps(fn):
    def hook(obj):
        return fn(obj, default=json_serial)

    return hook

def FixDump(fn):
    def hook(obj, fp):
        return fn(obj,fp, default=json_serial)

    return hook


json.dumps=FixDumps(json.dumps)
json.dump=FixDump(json.dump)


if __name__=="__main__":
    today=datetime.now()
    data={'atime':today, 'greet':'Hello'}
    str=json.dumps(data)
    print str

En important le module ci-dessus, mes autres modules utilisent json de manière normale (sans spécifier le mot clé par défaut) pour sérialiser des données contenant des objets date-heure. Le code du sérialiseur datetime est appelé automatiquement pour json.dumps et json.dump.

0
John Moore