Sérialisation de l'instance de classe en JSON

195

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

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

Un appel à json.dumps est effectué 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 également essayé d'utiliser le module pickle:

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

Et il 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?

ferhan
la source
32
Utilisez une ligne, s = json.dumps(obj, default=lambda x: x.__dict__)pour les variables d'instance d'objet serialize ( self.value1, self.value2, ...). C'est la manière la plus simple et la plus directe. Il sérialisera les structures d'objets imbriqués. La defaultfonction est appelée lorsqu'un objet donné n'est pas directement sérialisable. Vous pouvez également consulter ma réponse ci-dessous. J'ai trouvé les réponses populaires inutilement complexes, ce qui était probablement vrai il y a assez longtemps.
codeman48
2
Votre testclassn'a pas de __init__()méthode, donc toutes les instances partageront les deux mêmes attributs de classe ( value1et value2) définis dans l'instruction de classe. Comprenez-vous la différence entre une classe et une instance d'une?
martineau
1
Il existe une bibliothèque python pour ce github.com/jsonpickle/jsonpickle (commenter car la réponse est trop en dessous dans le fil de discussion et ne sera pas joignable.)
meilleurs voeux

Réponses:

246

Le problème de base est que l'encodeur JSON json.dumps()ne sait comment sérialiser qu'un ensemble limité de types d'objets 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 JSONEncoderla JSONEncoder.default()fonction, puis l'implémente , et que cette fonction émette le JSON correct pour votre classe.

Une solution simple serait de faire appel json.dumps()au .__dict__membre de cette instance. C'est un Python standard dictet 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 cet article de blog:

    Sérialisation d'objets Python arbitraires en JSON à l'aide de __dict__

Steveha
la source
3
J'ai essayé ça. Le résultat final d'un appel à json.dumps (t .__ dict__) est juste {}.
ferhan
6
C'est parce que votre classe n'a pas de .__init__()fonction de méthode, donc votre instance de classe a un dictionnaire vide. En d'autres termes, {}est le résultat correct pour votre exemple de code.
steveha
4
Merci. Cela fait l'affaire. J'ai ajouté un simple init sans paramètres et en appelant maintenant json.dumps (t .__ dict__) retourne les données appropriées au format: {"value2": "345", "value1": "123"} J'avais vu des articles comme cela avant, je ne savais pas si j'avais besoin d'un sérialiseur personnalisé pour les membres, le besoin d' initialiser n'était pas mentionné explicitement ou je l'ai manqué. Merci.
ferhan
4
Ce travail pour une seule classe mais pas avec des objets de classes connexes
Nwawel A Iroume
2
@NwawelAIroume: C'est vrai. Si vous avez un objet qui, par exemple, contient plusieurs objets dans une liste, l'erreur est toujoursis not JSON serializable
gies0r
63

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, ce 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 ifs sont pour la sérialisation de la date et de l'heure, puis il y a un obj.__dict__retour 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'à présent, cela a si bien fonctionné pour moi, dans l'attente de vos pensées.

brocoli
la source
Je reçois NameError: name 'serialize' is not defined. Des conseils?
Kyle Delaney
Très agréable. Juste pour les classes qui ont des créneaux horaires:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastory
C'est étonnant qu'un langage aussi populaire n'ait pas une seule ligne pour jsoniny un objet. Doit être parce qu'il n'est pas typé statiquement.
TheRennen le
57

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

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

Explication:

Formez les documents ( 2.7 , 3.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 instancevariables et non de classvariables, comme l'exemple de la question tente de le faire. (Je suppose que le demandeur est censé class instanceêtre un objet d'une classe)

J'ai appris cela en premier grâce à 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.

codeman48
la source
7
Cela a fonctionné pour moi, mais à cause des membres datetime.date, je l'ai légèrement changé:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins
@Dakota beau travail autour; datetime.dateest une implémentation C donc il n'a pas d' __dict__attribut. datetime.dateÀ
mon humble avis,
22

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'objets compliquée, vous n'obtiendrez certainement pas tout. Cependant, je l'utilise pour certains de mes objets simples.

Une sur laquelle cela fonctionne très bien est la classe "options" que vous obtenez du module OptionParser. Le 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)
SpiRail
la source
Vous voudrez peut-être supprimer self, si vous ne l'utilisez pas dans une classe.
SpiRail
3
Cela fonctionnera bien, tant que l'objet n'est pas composé d'autres objets.
Haroldo_OK
20

Utilisation de jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
gies0r
la source
5

JSON n'est pas vraiment destiné à sérialiser des objets Python arbitraires. C'est génial pour sérialiser des dictobjets, mais le picklemodule est vraiment ce que vous devriez utiliser en général. La sortie de picklen'est pas vraiment lisible par l'homme, mais elle devrait être décochée très bien. Si vous insistez pour utiliser JSON, vous pouvez consulter le jsonpicklemodule, qui est une approche hybride intéressante.

https://github.com/jsonpickle/jsonpickle

Bois Brendan
la source
9
Le principal problème que je vois avec pickle est qu'il s'agit d'un format spécifique à Python, tandis que JSON est un format indépendant de la plate-forme. JSON est particulièrement utile si vous écrivez une application Web ou un backend pour une application mobile. Cela étant dit, merci d'avoir signalé à jsonpickle.
Haroldo_OK
@Haroldo_OK Jsonpickle n'exporte-t-il pas toujours vers JSON, mais pas très lisible par l'homme?
Caelum
5

Python3.x

La meilleure approche que j'ai pu atteindre avec mes connaissances était celle-ci.
Notez que ce code traite également set ().
Cette approche est générique et nécessite simplement l'extension de classe (dans le deuxième exemple).
Notez que je ne fais que des 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 de différentes manières. Je suppose qu'un constructeur par défaut 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 un peu plus de recherche, j'ai trouvé un moyen de généraliser sans avoir besoin de l' appel de la méthode de registre SUPERCLASS , en utilisant une métaclasse

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ 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
Davi Abreu Wasserberg
la source
4

Voici deux fonctions simples pour la sérialisation de toutes les classes non sophistiquées, rien d'extraordinaire comme expliqué précédemment.

J'utilise ceci pour des trucs de type de configuration car je peux ajouter de nouveaux membres aux classes sans ajustement de 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}
GBGOLC
la source
3

Il y a quelques bonnes réponses sur la façon de commencer à faire cela. Mais il y a certaines choses à garder à l'esprit:

  • Que faire 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 souhaitez désérialiser l'instance?
  • Et si vous utilisez à la __slots__place de __dict__?
  • Et si vous ne voulez tout simplement pas le faire vous-même?

json-tricks est une bibliothèque (que j'ai créée et à laquelle d'autres ont contribué) 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-trickspour ne pas oublier certains cas particuliers (comme __slots__).

Il fait également d'autres types comme les tableaux numpy, datetimes, nombres complexes; il permet également les commentaires.

marque
la source
2

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

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

puis avoir une jsonEnc()fonction 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

Alors tu appelles json.dumps(classInstance,default=jsonDefEncoder)

hwat
la source
0

Vous pouvez utiliser jsonic pour sérialiser à peu près n'importe quoi en JSON:

https://github.com/OrrBin/Jsonic

Exemple:

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

instance = TestClass()
s = serialize(instance): # instance s set to: {"x":1, "y":2}
d = deserialize(s) # d is a new class instance of TestClass

pythonic a quelques fonctionnalités intéressantes comme la déclaration d'attributs de classe transitoire et la désérialisation sûre de type.

(Quelques années de retard avec la réponse, mais je pense que cela pourrait aider les autres)

Orr Benyamini
la source