Serializando la instancia de clase a JSON

186

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 a json.dumps se realiza 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 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é estoy haciendo mal?

ferhan
fuente
30
Utilice una línea, s = json.dumps(obj, default=lambda x: x.__dict__)para las variables de instancia de objeto serialize ( self.value1, self.value2, ...). Es la forma más sencilla y directa. Serializará estructuras de objetos anidados. Se defaultllama a la función cuando cualquier objeto dado no es directamente serializable. También puedes ver mi respuesta a continuación. Encontré las respuestas populares innecesariamente complejas, que probablemente eran ciertas hace mucho tiempo.
codeman48
1
No testclasstiene __init__()método, por lo que todas las instancias compartirán los mismos dos atributos de clase ( value1y value2) definidos en la declaración de clase. ¿Entiendes la diferencia entre una clase y una instancia de una?
Martineau
1
Hay una biblioteca de Python para este github.com/jsonpickle/jsonpickle (comentando desde respuesta es muy por debajo de la rosca y No será alcanzable.)
los mejores deseos

Respuestas:

238

El problema básico es que el codificador JSON json.dumps()solo sabe cómo serializar un conjunto limitado de tipos de objetos de forma predeterminada, todos los tipos incorporados. Lista aquí: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

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

Una solución simple sería llamar json.dumps()al .__dict__miembro de esa instancia. Ese es un Python estándar dicty 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 analiza en esta publicación de blog:

    Serializar objetos arbitrarios de Python a JSON usando __dict__

steveha
fuente
3
Intenté esto El resultado final de una llamada a json.dumps (t .__ dict__) es solo {}.
ferhan
66
Esto se debe a que su clase no tiene una .__init__()función de método, por lo que su instancia de clase tiene un diccionario vacío. En otras palabras, {}es el resultado correcto para su código de ejemplo.
steveha
3
Gracias. Esto hace el truco. Agregué un init simple sin parámetros y ahora llamando a json.dumps (t .__ dict__) devuelve los datos adecuados en el formato de: {"value2": "345", "value1": "123"} había visto publicaciones como esto antes, no estaba seguro de si necesitaba un serializador personalizado para los miembros, la necesidad de init no se mencionó explícitamente o la perdí. Gracias.
ferhan
3
Este trabajo para una sola clase pero no con objetos de clases relacionadas
Nwawel A Iroume
2
@NwawelAIroume: Cierto. Si tiene un objeto que, por ejemplo, contiene varios objetos en una lista, el error sigue siendois not JSON serializable
gies0r
57

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

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

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 primeros dos if son para la serialización de fecha y hora y luego hay un obj.__dict__ devuelve cualquier otro objeto.

la llamada final se ve así:

json.dumps(myObj, default=serialize)

Es especialmente bueno cuando está serializando una colección y no desea llamar __dict__ explícitamente a cada objeto. Aquí se hace automáticamente.

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

Brócoli
fuente
Consigo NameError: name 'serialize' is not defined. ¿Algun consejo?
Kyle Delaney
Muy agradable. Solo para clases que tienen máquinas tragamonedas:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastory
Es sorprendente que un lenguaje tan popular no tenga un revestimiento único para manipular un objeto. Debe ser porque no está estáticamente escrito.
TheRennen
49

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

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

Explicación:

Forme 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, necesita instancevariables y no classvariables, como intenta hacer el ejemplo en la pregunta. (Supongo que el autor de la pregunta pretende class instanceser un objeto de una clase)

Aprendí esto primero de la respuesta de @ phihag aquí . Descubrí que es la forma más sencilla y limpia de hacer el trabajo.

codeman48
fuente
66
Esto funcionó para mí, pero debido a los miembros datetime.date lo cambié ligeramente:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins
@Dakota buena solución; datetime.datees una implementación en C, por lo tanto, no tiene ningún __dict__atributo. En mi humilde opinión por el bien de la uniformidad, datetime.datedebería tenerlo ...
codeman48
22

Solamente lo hago:

data=json.dumps(myobject.__dict__)

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

Uno en el que funciona muy bien es la clase de "opciones" que obtienes 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)
SpiRail
fuente
Es posible que desee eliminar self, si no está usando esto dentro de una clase.
SpiRail
3
Eso funcionará bien, siempre y cuando el objeto no esté compuesto de otros objetos.
Haroldo_OK
19

Usando jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
gies0r
fuente
5

JSON no está realmente destinado a serializar objetos arbitrarios de Python. Es ideal para serializar dictobjetos, pero el picklemódulo es realmente lo que debería estar usando en general. La salida de pickleno es realmente legible para el ser humano, pero debería desaparecer bien. Si insiste en usar JSON, puede consultar eljsonpickle módulo, que es un enfoque híbrido interesante.

https://github.com/jsonpickle/jsonpickle

Madera de Brendan
fuente
9
El principal problema que veo con pickle es que es un formato específico de Python, mientras que JSON es un formato independiente de la plataforma. JSON es especialmente útil si está escribiendo una aplicación web o un back-end para alguna aplicación móvil. Dicho esto, gracias por señalar a jsonpickle.
Haroldo_OK
@ Haroldo_OK ¿Todavía no se exporta jsonpickle a JSON, solo que no es muy legible para humanos?
Caelum
4

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

Lo uso para el 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}
GBGOLC
fuente
3

Hay algunas buenas respuestas sobre cómo comenzar a hacer esto. Pero hay algunas cosas a tener en cuenta:

  • ¿Qué sucede si la instancia está anidada dentro de una estructura de datos grande?
  • ¿Qué pasa si también quieres el nombre de la clase?
  • ¿Qué pasa si quieres deserializar la instancia?
  • ¿Qué pasa si estás usando en __slots__lugar de__dict__ ?
  • ¿Qué pasa si simplemente no quieres hacerlo tú mismo?

json-tricks es una biblioteca (que hice y a la que contribuyeron otros) que ha podido 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 desea hacer su propia solución, puede mirar la fuente json-trickspara no olvidar algunos casos especiales (como__slots__ ).

También hace otros tipos como matrices numpy, fechas, números complejos; También permite comentarios.

marca
fuente
3

Python3.x

El mejor enfoque que pude alcanzar con mi conocimiento fue este.
Tenga en cuenta que este código trata también a set ().
Este enfoque es genérico y solo necesita la extensión de 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 CoDec.

Con un poco más de trabajo puedes construir tu clase de otras maneras. Asumo un constructor predeterminado para instanciarlo, luego actualizo la 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
Davi Abreu Wasserberg
fuente
2

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 instrucción if else para personalizar la codificación de cada objeto. Eso significa crear un codificador genérico predeterminado para JSON como:

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

y luego tiene una jsonEnc()función en cada clase que desea serializar. p.ej

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

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

hwat
fuente