web-dev-qa-db-fra.com

Sérialisation d'une instance de classe en JSON

J'essaie de créer une représentation sous forme de chaîne JSON d'une instance de classe et rencontre des difficultés. Disons que la classe est construite comme ceci:

class testclass:
    value1 = "a"
    value2 = "b"

Un appel à json.dumps est fait comme ceci:

t = testclass()
json.dumps(t)

Il échoue et me dit que la classe de test n'est pas sérialisable JSON.

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

J'ai aussi essayé d'utiliser le module cornichon:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

Et cela donne des informations sur l'instance de classe, mais pas un contenu sérialisé de l'instance de classe.

b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

Qu'est-ce que je fais mal?

155
ferhan

Le problème de base est que l'encodeur JSON json.dumps() ne sait comment sérialiser qu'un ensemble limité de types d'objet par défaut, tous les types intégrés. Liste ici: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Une bonne solution serait de faire en sorte que votre classe hérite de JSONEncoder, puis implémente la fonction JSONEncoder.default() et que cette fonction émette le code JSON correct pour votre classe.

Une solution simple consisterait à appeler json.dumps() sur le membre .__dict__ de cette instance. C'est un standard Python dict et si votre classe est simple, elle sera sérialisable JSON.

class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

L'approche ci-dessus est discutée dans ce billet de blog:

Sérialisation d'objets Python arbitraires sur JSON à l'aide de __dict __

204
steveha

Il y a une façon qui fonctionne très bien pour moi que vous pouvez essayer:

json.dumps() peut prendre un paramètre facultatif par défaut où vous pouvez spécifier une fonction de sérialiseur personnalisée pour les types inconnus, qui dans mon cas ressemble à

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

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

Les deux premiers if concernent la sérialisation de la date et de l'heure, puis un obj.__dict__ est renvoyé pour tout autre objet.

l'appel final ressemble à:

json.dumps(myObj, default=serialize)

C'est particulièrement utile lorsque vous sérialisez une collection et que vous ne voulez pas appeler __dict__ explicitement pour chaque objet. Ici c'est fait pour vous automatiquement.

Jusqu'ici a travaillé si bien pour moi, dans l'attente de vos pensées.

48
Broccoli

Vous pouvez spécifier le paramètre nommé default dans la fonction json.dumps():

json.dumps(obj, default=lambda x: x.__dict__)

Explication:

Former les docs ( 2.7 , .6 ):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(Fonctionne sur Python 2.7 et Python 3.x)

Remarque: dans ce cas, vous avez besoin de variables instance et non de variables class, comme le montre l'exemple de la question. (Je suppose que le demandeur voulait dire class instance être un objet d'une classe)

J'ai d'abord appris cela de la réponse de @ phihag ici . J'ai trouvé que c'était le moyen le plus simple et le plus propre de faire le travail.

37
codeman48

Je fais juste:

data=json.dumps(myobject.__dict__)

Ce n'est pas la réponse complète, et si vous avez une sorte de classe d'objet compliquée, vous n'obtiendrez certainement pas tout. Cependant, je l'utilise pour certains de mes objets simples.

La classe "options" du module OptionParser est particulièrement efficace. La voici avec la requête JSON elle-même.

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)
22
SpiRail

Utilisation de jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
14
gies0r

Voici deux fonctions simples pour la sérialisation de toute classe non sophistiquée, rien d’extraordinaire comme expliqué précédemment.

J'utilise ceci pour le type de configuration car je peux ajouter de nouveaux membres aux classes sans ajustement du code.

import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}
4
GBGOLC

JSON n'est pas vraiment destiné à la sérialisation d'objets Python arbitraires. C'est bien pour sérialiser les objets dict, mais le module pickle est vraiment ce que vous devriez utiliser en général. La sortie de pickle n'est pas vraiment lisible par l'homme, mais elle devrait se décoiffer correctement. Si vous insistez pour utiliser JSON, vous pouvez consulter le module jsonpickle, qui constitue une approche hybride intéressante.

https://github.com/jsonpickle/jsonpickle

3
Brendan Wood

Je crois qu'au lieu de l'héritage comme suggéré dans la réponse acceptée, il est préférable d'utiliser le polymorphisme. Sinon, vous devez avoir une instruction big if else pour personnaliser le codage de chaque objet. Cela signifie créer un encodeur générique par défaut pour JSON en tant que:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

et ensuite avoir une fonction jsonEnc() dans chaque classe que vous souhaitez sérialiser. par exemple.

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

Ensuite, vous appelez json.dumps(classInstance,default=jsonDefEncoder)

2
hwat

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 et ne nécessite que l’extension de 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, il s'agit d'un CoDec.

Avec un peu plus de travail, vous pouvez construire votre classe d’une autre manière. Je suppose un constructeur par défaut pour l'instance, 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)

Éditer

Avec quelques recherches supplémentaires, j'ai trouvé un moyen de généraliser sans recourir à l'appel de la méthode SUPERCLASS, en utilisant un métaclasse

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
1

Il existe de bonnes réponses sur la façon de s'y prendre. Mais il y a certaines choses à garder à l'esprit:

  • Que se passe-t-il si l'instance est imbriquée dans une grande structure de données?
  • Et si vous voulez aussi le nom de la classe?
  • Que faire si vous voulez désérialiser l'instance?
  • Que faire si vous utilisez __slots__ au lieu de __dict__?
  • Et si vous ne voulez pas le faire vous-même?

json-tricks est une bibliothèque (que j'ai créée et à laquelle j'ai contribué) et qui est capable de le faire depuis un certain temps. Par exemple:

class MyTestCls:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

cls_instance = MyTestCls(s='ub', dct={'7': 7})

json = dumps(cls_instance, indent=4)
instance = loads(json)

Vous récupérerez votre instance. Ici le json ressemble à ceci:

{
    "__instance_type__": [
        "json_tricks.test_class",
        "MyTestCls"
    ],
    "attributes": {
        "s": "ub",
        "dct": {
            "7": 7
        }
    }
}

Si vous aimez créer votre propre solution, vous pouvez regarder la source de json-tricks afin de ne pas oublier certains cas particuliers (comme __slots__).

Il fait aussi d’autres types comme les tableaux numpy, les datetime, les nombres complexes; cela permet aussi des commentaires.

1
Mark