Serializar instancia de clase a JSON


Estoy tratando de crear una representación de cadena JSON de una instancia de clase y tengo dificultades. Digamos que la clase se construye así:

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

Una llamada al json.dumps se hace así:

t = testclass()
json.dumps(t)

Está fallando y me dice que la clase de prueba no es serializable JSON.

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

También he intentado usar el módulo pickle:

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

Y proporciona información de la instancia de clase, pero no un contenido serializado de la instancia de clase.

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

¿Qué soy haciendo mal?

Author: martineau, 2012-04-20

10 answers

El problema básico es que el codificador JSON json.dumps() solo sabe serializar un conjunto limitado de tipos de objeto por defecto, todos los tipos incorporados. Listar aquí: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Una buena solución sería hacer que su clase herede de JSONEncoder y luego implementar la función JSONEncoder.default(), y hacer que esa función emita el JSON correcto para su clase.

Una solución simple sería llamar {[1] } al .__dict__ miembro de ese instancia. Eso es un Python estándar dict y si su clase es simple será serializable 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}

El enfoque anterior se discute en esta publicación del blog:

Serializar objetos Python arbitrarios a JSON usando _ _ dict__

NOTA: He editado esta respuesta; la versión original solo discutió el enfoque de serialización .__dict__.

 146
Author: steveha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-30 01:55:19

Hay una manera que funciona muy bien para mí que puedes probar:

json.dumps() puede tomar un parámetro opcional default donde puede especificar una función serializer personalizada para tipos desconocidos, que en mi caso se parece a

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__

Los dos primeros ifs son para serialización de fecha y hora y luego hay un obj.__dict__ devuelto para cualquier otro objeto.

La llamada final se ve como:

json.dumps(myObj, default=serialize)

Es especialmente bueno cuando estás serializando una colección y no quieres llame a __dict__ explícitamente para cada objeto. Aquí se hace para usted automáticamente.

Hasta ahora funcionó tan bien para mí, esperando sus pensamientos.

 25
Author: Broccoli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-17 16:25:13

Simplemente lo hago:

data=json.dumps(myobject.__dict__)

Esta no es la respuesta completa, y si tiene algún tipo de clase de objeto complicada, ciertamente no obtendrá todo. Sin embargo, uso esto para algunos de mis objetos simples.

Una en la que funciona muy bien es la clase "options" que se obtiene del módulo OptionParser. Aquí está junto con la solicitud JSON en sí.

  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)
 19
Author: SpiRail,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-10 14:36:02

Puede especificar el parámetro con nombre default en la función json.dumps():

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

Explicación:

Formar los documentos (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.

(Funciona en Python 2.7 y Python 3.x)

Nota: En este caso necesitas instance variables y no class variables, como el ejemplo de la pregunta intenta hacer. (Estoy asumiendo que el asker significaba class instance ser un objeto de una clase)

Aprendí esto primero de la respuesta de @phihag aquí. Lo encontré para ser el la forma más sencilla y limpia de hacer el trabajo.

 12
Author: codeman48,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-27 10:28:59

Usando jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
 5
Author: gies0r,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-05 18:02:44

JSON no está realmente destinado a serializar objetos Python arbitrarios. Es genial para serializar objetos dict, pero el módulo pickle es realmente lo que deberías usar en general. La salida de pickle no es realmente legible por humanos, pero debería desempaquetarse bien. Si insistes en usar JSON, podrías revisar el módulo jsonpickle, que es un enfoque híbrido interesante.

Https://github.com/jsonpickle/jsonpickle

 3
Author: Brendan Wood,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-20 19:13:39

Aquí hay dos funciones simples para la serialización de cualquier clase no sofisticada, nada elegante como se explicó antes.

Uso esto para cosas de tipo de configuración porque puedo agregar nuevos miembros a las clases sin ajustes de código.

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}
 2
Author: GBGOLC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-10 17:23:07

Creo que en lugar de la herencia como se sugiere en la respuesta aceptada, es mejor usar polimorfismo. De lo contrario, debe tener una gran sentencia if else para personalizar la codificación de cada objeto. Eso significa crear un codificador genérico por defecto para JSON como:

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

Y luego tener una función jsonEnc() en cada clase que desee serializar. por ejemplo,

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

Luego llamas json.dumps(classInstance,default=jsonDefEncoder)

 1
Author: hwat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-04 21:44:00

Python3.x

El mejor enfoque que pude alcanzar con mi conocimiento fue este.
Tenga en cuenta que este código también trata a set ().
Este enfoque es genérico solo necesita la extensión de la clase (en el segundo ejemplo).
Tenga en cuenta que solo lo estoy haciendo a los archivos, pero es fácil modificar el comportamiento a su gusto.

Sin embargo, este es un CóDec.

Con un poco más de trabajo puedes construir tu clase de otras maneras. Asumo un constructor predeterminado para instarlo, luego actualizo el clase 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)

Editar

Con un poco más de investigación encontré una manera de generalizar sin la necesidad de la llamada al método de registro SUPERCLASS, usando una metaclase

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
 1
Author: Davi Abreu Wasserberg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-19 05:48:37

Hay algunas buenas respuestas sobre cómo empezar a hacerlo. Pero hay algunas cosas a tener en cuenta:

  • ¿Qué pasa si la instancia está anidada dentro de una estructura de datos grande?
  • ¿Y si también quieres el nombre de la clase?
  • ¿Qué pasa si desea deserializar la instancia?
  • ¿Qué pasa si usas __slots__ en lugar de __dict__?
  • ¿Qué pasa si simplemente no quieres hacerlo tú mismo?

Json-tricks es una biblioteca (que hice y otros contribuido a) que ha sido capaz de hacer esto durante bastante tiempo. Por ejemplo:

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)

Recuperarás tu instancia. Aquí el json se ve así:

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

Si quieres hacer tu propia solución, puedes mirar la fuente de json-tricks para no olvidar algunos casos especiales (como __slots__).

También hace otros tipos como arrays numpy, datetimes, números complejos; también permite comentarios.

 0
Author: Mark,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-19 18:11:12