Hacer que el objeto JSON sea serializable con un codificador normal
La forma habitual de serializar JSON objetos personalizados no serializables es crear subclases json.JSONEncoder
y luego pasar un codificador personalizado a json.dumps()
.
Suele tener este aspecto:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Foo):
return obj.to_json()
return json.JSONEncoder.default(self, obj)
print(json.dumps(obj, cls=CustomEncoder))
Lo que intento hacer es hacer algo serializable con el codificador predeterminado. Miré a mi alrededor pero no pude encontrar nada. Mi idea es que habría algún campo en el que el codificador mira para determinar la codificación json. Algo parecido a __str__
. Quizás un __json__
campo. ¿Existe algo como esto en Python?
Quiero que una clase de un módulo que estoy creando sea serializable en JSON para todos los que usan el paquete sin que se preocupen por implementar sus propios codificadores personalizados [triviales].
Como dije en un comentario a tu pregunta, después de mirar el json
código fuente del módulo, no parece prestarse a hacer lo que deseas. Sin embargo, el objetivo podría lograrse mediante lo que se conoce como parche de mono
(consulte la pregunta ¿Qué es un parche de mono? ). Esto podría hacerse en __init__.py
el script de inicialización de su paquete y afectaría toda json
la serialización posterior del módulo, ya que los módulos generalmente solo se cargan una vez y el resultado se almacena en caché sys.modules
.
El parche cambia el método del codificador json predeterminado default
: el default()
.
A continuación se muestra un ejemplo implementado como un módulo independiente en aras de la simplicidad:
Módulo:make_json_serializable.py
""" Module that monkey-patches json module when it's imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder.default # Save unmodified default.
JSONEncoder.default = _default # Replace it.
Usarlo es trivial ya que el parche se aplica simplemente importando el módulo.
Script de cliente de muestra:
import json
import make_json_serializable # apply monkey-patch
class Foo(object):
def __init__(self, name):
self.name = name
def to_json(self): # New special method.
""" Convert to JSON format string representation. """
return '{"name": "%s"}' % self.name
foo = Foo('sazpaz')
print(json.dumps(foo)) # -> "{\"name\": \"sazpaz\"}"
Para conservar la información del tipo de objeto, el método especial también puede incluirla en la cadena devuelta:
return ('{"type": "%s", "name": "%s"}' %
(self.__class__.__name__, self.name))
Lo que produce el siguiente JSON que ahora incluye el nombre de la clase:
"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"
La magia yace aquí
Incluso mejor que hacer que el reemplazo default()
busque un método con un nombre especial sería que pudiera serializar la mayoría de los objetos de Python automáticamente , incluidas las instancias de clases definidas por el usuario, sin necesidad de agregar un método especial. Después de investigar una serie de alternativas, la siguiente, basada en una respuesta de @Raymond Hettinger a otra pregunta, que utiliza el pickle
módulo, me pareció la más cercana a ese ideal:
Módulo:make_json_serializable2.py
""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle
def _default(self, obj):
return {'_python_object': pickle.dumps(obj)}
JSONEncoder.default = _default # Replace with the above.
Por supuesto, no todo se puede conservar en vinagre, por ejemplo, los tipos de extensiones. Sin embargo, hay formas definidas de manejarlos a través del protocolo pickle escribiendo métodos especiales, similares a lo que usted sugirió y que describí anteriormente, pero hacerlo probablemente sería necesario para un número mucho menor de casos.
Deserializar
De todos modos, usar el protocolo pickle también significa que sería bastante fácil reconstruir el objeto Python original proporcionando un object_hook
argumento de función personalizado en cualquier json.loads()
llamada que use cualquier '_python_object'
clave del diccionario pasada, siempre que tenga una. Algo como:
def as_python_object(dct):
try:
return pickle.loads(str(dct['_python_object']))
except KeyError:
return dct
pyobj = json.loads(json_str, object_hook=as_python_object)
Si esto tiene que hacerse en muchos lugares, podría valer la pena definir una función contenedora que proporcione automáticamente el argumento de palabra clave adicional:
json_pkloads = functools.partial(json.loads, object_hook=as_python_object)
pyobj = json_pkloads(json_str)
json
Naturalmente, esto también podría parchearse en el módulo, haciendo que la función sea la predeterminada object_hook
(en lugar de None
).
Se me ocurrió la idea de usarlo pickle
a partir de una respuesta de Raymond Hettinger a otra pregunta sobre serialización de JSON, a quien considero excepcionalmente creíble y también una fuente oficial (como en el desarrollador principal de Python).
Portabilidad a Python 3
El código anterior no funciona como se muestra en Python 3 porque json.dumps()
devuelve un bytes
objeto que JSONEncoder
no puede manejar. Sin embargo, el enfoque sigue siendo válido. Una forma sencilla de solucionar el problema es latin1
"decodificar" el valor devuelto pickle.dumps()
y luego "codificarlo" latin1
antes de pasarlo a pickle.loads()
la as_python_object()
función. Esto funciona porque las cadenas binarias arbitrarias son válidas latin1
y siempre se pueden decodificar en Unicode y luego volver a codificarlas en la cadena original (como lo señala Sven Marnach en esta respuesta ).
(Aunque lo siguiente funciona bien en Python 2, la latin1
decodificación y codificación que realiza es superflua).
from decimal import Decimal
class PythonObjectEncoder(json.JSONEncoder):
def default(self, obj):
return {'_python_object': pickle.dumps(obj).decode('latin1')}
def as_python_object(dct):
try:
return pickle.loads(dct['_python_object'].encode('latin1'))
except KeyError:
return dct
class Foo(object): # Some user-defined class.
def __init__(self, name):
self.name = name
def __eq__(self, other):
if type(other) is type(self): # Instances of same class?
return self.name == other.name
return NotImplemented
__hash__ = None
data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
Foo('Bar'), Decimal('3.141592653589793238462643383279502884197169')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2 # both should be same
Puedes extender la clase dict así:
#!/usr/local/bin/python3
import json
class Serializable(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# hack to fix _json.so make_encoder serialize properly
self.__setitem__('dummy', 1)
def _myattrs(self):
return [
(x, self._repr(getattr(self, x)))
for x in self.__dir__()
if x not in Serializable().__dir__()
]
def _repr(self, value):
if isinstance(value, (str, int, float, list, tuple, dict)):
return value
else:
return repr(value)
def __repr__(self):
return '<%s.%s object at %s>' % (
self.__class__.__module__,
self.__class__.__name__,
hex(id(self))
)
def keys(self):
return iter([x[0] for x in self._myattrs()])
def values(self):
return iter([x[1] for x in self._myattrs()])
def items(self):
return iter(self._myattrs())
Ahora, para que tus clases sean serializables con el codificador normal, extiende 'Serializable':
class MySerializableClass(Serializable):
attr_1 = 'first attribute'
attr_2 = 23
def my_function(self):
print('do something here')
obj = MySerializableClass()
print(obj)
imprimirá algo como:
<__main__.MySerializableClass object at 0x1073525e8>
print(json.dumps(obj, indent=4))
imprimirá algo como:
{
"attr_1": "first attribute",
"attr_2": 23,
"my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}