Haciendo objeto JSON serializable con codificador regular

La forma habitual de serializar objetos JSON no serializables mediante JSON es subclase json.JSONEncoder y luego pasar un codificador personalizado a los volcados.

Por lo general, se ve así:

 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 estoy tratando de hacer es hacer algo serializable con el codificador predeterminado. Miré a mi alrededor pero no pude encontrar nada. Mi pensamiento es que habría algún campo en el que el codificador mira para determinar la encoding json. Algo similar a __str__ . Quizás un campo __json__ . ¿Hay algo como esto en Python?

Quiero hacer que una clase de un módulo que estoy haciendo sea JSON serializable para todos los que usan el paquete sin que se preocupen por implementar sus propios codificadores personalizados [triviales].

Como dije en un comentario a su pregunta, después de mirar el código fuente del módulo json , no parece que se preste para hacer lo que quiere. Sin embargo, el objective podría lograrse mediante lo que se conoce como parches de mono (ver pregunta ¿Qué es un parche de mono? ). Esto se podría hacer en el __init__.py inicialización __init__.py su paquete y afectaría toda la serialización del módulo json posterior, ya que los módulos generalmente solo se cargan una vez y el resultado se almacena en caché en sys.modules .

El parche cambia el método default del codificador json default : el predeterminado default() .

Aquí hay un ejemplo implementado como un módulo independiente para simplificar:

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. 

Su uso es trivial ya que el parche se aplica simplemente importando el módulo.

Ejemplo de script de cliente:

 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 incluirlo 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 miente aqui

Incluso mejor que tener el reemplazo default() para buscar un método con un nombre especial, sería para que pueda serializar la mayoría de los objetos de Python automáticamente , incluidas las instancias de clase definidas por el usuario, sin necesidad de agregar un método especial. Después de investigar una serie de alternativas, las siguientes que usan el módulo pickle parecieron más cercanas 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, todo no puede ser decapado, por ejemplo, tipos de extensión Sin embargo, hay formas definidas para manejarlos a través del protocolo pickle escribiendo métodos especiales, similares a lo que sugirió y describí anteriormente, pero es probable que esto sea necesario para un número mucho menor de casos.

En cualquier caso, usar el protocolo pickle también significa que sería bastante fácil reconstruir el objeto Python original al proporcionar un argumento personalizado de función object_hook en cualquier json.loads() que buscara una clave '_python_object' en el diccionario pasado. Algo como :

 def as_python_object(dct): if '_python_object' in dct: return pickle.loads(str(dct['_python_object'])) 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 de envoltorio 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) 

Naturalmente, esto también podría ser parcheado en el módulo json , haciendo que la función sea el object_hook predeterminado (en lugar de None ).

Tuve la idea de utilizar pickle desde una respuesta de Raymond Hettinger a otra pregunta de serialización JSON, a quien considero excepcionalmente creíble, así como una fuente oficial (como en el desarrollador central de Python).

Portablity a Python 3

El código anterior no funciona como se muestra en Python 3 porque json.dumps() devuelve un objeto de bytes que el 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 por pickle.dumps() y luego “codificarlo” desde latin1 antes de pasarlo a pickle.loads() en la función as_python_object() . Esto funciona porque las cadenas binarias arbitrarias son válidas latin1 que siempre pueden decodificarse a Unicode y luego codificarse de nuevo a la cadena original (como lo señala Sven Marnach en esta respuesta ).

(Aunque lo siguiente funciona bien en Python 2, la deencoding y encoding de latin1 que hace 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): if '_python_object' in dct: return pickle.loads(dct['_python_object'].encode('latin1')) return dct data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')] 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 como:

 #!/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 sus clases sean serializables con el codificador normal, extienda ‘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": ">" } 

Sugiero poner el hack en la definición de clase. De esta manera, una vez que se define la clase, soporta JSON. Ejemplo:

 import json class MyClass( object ): def _jsonSupport( *args ): def default( self, xObject ): return { 'type': 'MyClass', 'name': xObject.name() } def objectHook( obj ): if 'type' not in obj: return obj if obj[ 'type' ] != 'MyClass': return obj return MyClass( obj[ 'name' ] ) json.JSONEncoder.default = default json._default_decoder = json.JSONDecoder( object_hook = objectHook ) _jsonSupport() def __init__( self, name ): self._name = name def name( self ): return self._name def __repr__( self ): return '' % self._name myObject = MyClass( 'Magneto' ) jsonString = json.dumps( [ myObject, 'some', { 'other': 'objects' } ] ) print "json representation:", jsonString decoded = json.loads( jsonString ) print "after decoding, our object is the first in the list", decoded[ 0 ] 

El problema con la anulación de JSONEncoder().default es que solo puede hacerlo una vez. Si te topas con algo, un tipo de datos especial que no funciona con ese patrón (como si usas una encoding extraña). Con el patrón a continuación, siempre puede hacer que su clase JSON sea serializable, siempre que el campo de clase que desea serializar sea serializable en sí mismo (y se pueda agregar a una lista de python, apenas cualquier cosa). De lo contrario, debe aplicar recursivamente el mismo patrón a su campo json (o extraer los datos serializables):

 # base class that will make all derivatives JSON serializable: class JSONSerializable(list): # need to derive from a serializable class. def __init__(self, value = None): self = [ value ] def setJSONSerializableValue(self, value): self = [ value ] def getJSONSerializableValue(self): return self[1] if len(self) else None # derive your classes from JSONSerializable: class MyJSONSerializableObject(JSONSerializable): def __init__(self): # or any other function # .... # suppose your__json__field is the class member to be serialized. # it has to be serializable itself. # Every time you want to set it, call this function: self.setJSONSerializableValue(your__json__field) # ... # ... and when you need access to it, get this way: do_something_with_your__json__field(self.getJSONSerializableValue()) # now you have a JSON default-serializable class: a = MyJSONSerializableObject() print json.dumps(a) 

No entiendo por qué no puedes escribir una función de serialize para tu propia clase. Implementas el codificador personalizado dentro de la propia clase y permites que “personas” llamen a la función de serialización que esencialmente devolverá self.__dict__ con funciones eliminadas.

editar:

Esta pregunta coincide conmigo, que la forma más sencilla es escribir su propio método y devolver los datos serializados json que desee. También recomiendan probar jsonpickle, pero ahora está agregando una dependencia adicional para la belleza cuando se incorpore la solución correcta.

Para el entorno de producción, prepare un módulo de json propio con su propio codificador personalizado, para dejar en claro que anula algo. No se recomienda el parche de mono, pero puedes hacer un parche de mono en tu testenv.

Por ejemplo,

 class JSONDatetimeAndPhonesEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (datetime.date, datetime.datetime)): return obj.date().isoformat() elif isinstance(obj, basestring): try: number = phonenumbers.parse(obj) except phonenumbers.NumberParseException: return json.JSONEncoder.default(self, obj) else: return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL) else: return json.JSONEncoder.default(self, obj) 

usted quiere:

payload = json.dumps (your_data, cls = JSONDatetimeAndPhonesEncoder)

o:

payload = your_dumps (your_data)

o:

payload = your_json.dumps (your_data)

Sin embargo, en el entorno de prueba, ve a la cabeza:

 @pytest.fixture(scope='session', autouse=True) def testenv_monkey_patching(): json._default_encoder = JSONDatetimeAndPhonesEncoder() 

que aplicará su codificador a todas json.dumps apariciones json.dumps .