Python JSON serializar un objeto decimal


Tengo un Decimal('3.9') como parte de un objeto, y deseo codificar esto en una cadena JSON que debería parecerse a {'x': 3.9}. No me importa la precisión en el lado del cliente, así que un flotador está bien.

¿Hay una buena manera de serializar esto? JSONDecoder no acepta objetos decimales, y convertir a un float de antemano produce {'x': 3.8999999999999999} lo cual es incorrecto, y será un gran desperdicio de ancho de banda.

 168
Author: Knio, 2009-12-25

14 answers

¿Qué tal la subclase json.JSONEncoder?

class DecimalEncoder(json.JSONEncoder):
    def _iterencode(self, o, markers=None):
        if isinstance(o, decimal.Decimal):
            # wanted a simple yield str(o) in the next line,
            # but that would mean a yield on the line with super(...),
            # which wouldn't work (see my comment below), so...
            return (str(o) for o in [o])
        return super(DecimalEncoder, self)._iterencode(o, markers)

Entonces úsalo así:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
 110
Author: Michał Marczyk,
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
2009-12-25 07:14:05

Simplejson 2.1 y superior tiene soporte nativo para el tipo decimal:

>>> json.dumps(Decimal('3.9'), use_decimal=True)
'3.9'

Tenga en cuenta que use_decimal es True por defecto:

def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
    allow_nan=True, cls=None, indent=None, separators=None,
    encoding='utf-8', default=None, use_decimal=True,
    namedtuple_as_object=True, tuple_as_array=True,
    bigint_as_string=False, sort_keys=False, item_sort_key=None,
    for_json=False, ignore_nan=False, **kw):

Así que:

>>> json.dumps(Decimal('3.9'))
'3.9'

Con suerte, esta característica se incluirá en la biblioteca estándar.

 167
Author: Lukas Cenovsky,
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-02-23 16:36:38

Me gustaría que todos sepan que probé la respuesta de Michał Marczyk en mi servidor web que ejecutaba Python 2.6.5 y funcionó bien. Sin embargo, actualicé a Python 2.7 y dejó de funcionar. Traté de pensar en algún tipo de forma de codificar objetos decimales y esto es lo que se me ocurrió:

class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            return float(o)
        return super(DecimalEncoder, self).default(o)

Esto debería ayudar a cualquiera que tenga problemas con Python 2.7. Lo probé y parece que funciona bien. Si alguien nota algún error en mi solución o se le ocurre una mejor way, por favor házmelo saber.

 122
Author: Elias Zamaria,
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
2010-10-07 19:29:03

Intenté cambiar de simplejson a builtin json para GAE 2.7, y tuve problemas con el decimal. Si el valor predeterminado devuelve str (o) había comillas (porque _iterencode llama a _iterencode en los resultados del valor predeterminado), y float(o) eliminaría el 0 final.

Si por defecto devuelve un objeto de una clase que hereda de float (o cualquier cosa que llame a repr sin formato adicional) y tiene un método __repr__ personalizado, parece funcionar como yo quiero.

import json
from decimal import Decimal

class fakefloat(float):
    def __init__(self, value):
        self._value = value
    def __repr__(self):
        return str(self._value)

def defaultencode(o):
    if isinstance(o, Decimal):
        # Subclass float with custom repr?
        return fakefloat(o)
    raise TypeError(repr(o) + " is not JSON serializable")

json.dumps([10.20, "10.20", Decimal('10.20')], default=defaultencode)
'[10.2, "10.20", 10.20]'
 19
Author: tesdal,
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
2011-11-25 21:17:24

En mi aplicación Flask, Que usa python 2.7.11, flask alchemy(con 'db.decimal' types), y Flask Marshmallow (para' instant ' serializer y deserializer), tuve este error, cada vez que hice un GET o POST. El serializador y el deserializador no pudieron convertir los tipos decimales a ningún formato identificable con JSON.

Hice un "pip install simplejson", entonces Simplemente añadiendo

import simplejson as json

El serializador y el deserializador comienzan a ronronear de nuevo. No hice nada más... Los DECIAML se muestran como formato flotante '234.00'.

 18
Author: ISONecroMAn,
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-08-31 19:21:54

3.9 no se puede representar exactamente en los flotadores IEEE, siempre vendrá como 3.8999999999999999, por ejemplo, try print repr(3.9), puede leer más al respecto aquí:

Http://en.wikipedia.org/wiki/Floating_point
http://docs.sun.com/source/806-3568/ncg_goldberg.html

Así que si no quieres float, la única opción que tienes es enviarla como string, y para permitir la conversión automática de objetos decimales a JSON, haz algo como esto:

import decimal
from django.utils import simplejson

def json_encode_decimal(obj):
    if isinstance(obj, decimal.Decimal):
        return str(obj)
    raise TypeError(repr(obj) + " is not JSON serializable")

d = decimal.Decimal('3.5')
print simplejson.dumps([d], default=json_encode_decimal)
 9
Author: Anurag Uniyal,
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
2014-08-28 16:39:05

Mi $.02!

Extiendo un montón del codificador JSON ya que estoy serializando toneladas de datos para mi servidor web. Aquí tienes un buen código. Tenga en cuenta que es fácilmente extensible a casi cualquier formato de datos que desee y reproducirá 3.9 como "thing": 3.9

JSONEncoder_olddefault = json.JSONEncoder.default
def JSONEncoder_newdefault(self, o):
    if isinstance(o, UUID): return str(o)
    if isinstance(o, datetime): return str(o)
    if isinstance(o, time.struct_time): return datetime.fromtimestamp(time.mktime(o))
    if isinstance(o, decimal.Decimal): return str(o)
    return JSONEncoder_olddefault(self, o)
json.JSONEncoder.default = JSONEncoder_newdefault

Hace mi vida mucho más fácil...

 8
Author: std''OrgnlDave,
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-27 05:35:13

Esto es lo que tengo, extraído de nuestra clase

class CommonJSONEncoder(json.JSONEncoder):

    """
    Common JSON Encoder
    json.dumps(myString, cls=CommonJSONEncoder)
    """

    def default(self, obj):

        if isinstance(obj, decimal.Decimal):
            return {'type{decimal}': str(obj)}

class CommonJSONDecoder(json.JSONDecoder):

    """
    Common JSON Encoder
    json.loads(myString, cls=CommonJSONEncoder)
    """

    @classmethod
    def object_hook(cls, obj):
        for key in obj:
            if isinstance(key, six.string_types):
                if 'type{decimal}' == key:
                    try:
                        return decimal.Decimal(obj[key])
                    except:
                        pass

    def __init__(self, **kwargs):
        kwargs['object_hook'] = self.object_hook
        super(CommonJSONDecoder, self).__init__(**kwargs)

Que pasa unittest:

def test_encode_and_decode_decimal(self):
    obj = Decimal('1.11')
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': Decimal('1.11')}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)

    obj = {'test': {'abc': Decimal('1.11')}}
    result = json.dumps(obj, cls=CommonJSONEncoder)
    self.assertTrue('type{decimal}' in result)
    new_obj = json.loads(result, cls=CommonJSONDecoder)
    self.assertEqual(new_obj, obj)
 6
Author: James Lin,
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
2015-02-02 01:30:54

Falta la opción nativa, así que la agregaré para el próximo chico/gall que la busque.

A partir de Django 1.7.x hay un DjangoJSONEncoder incorporado que puedes obtener de django.core.serializers.json.

import json
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict

model_instance = YourModel.object.first()
model_dict = model_to_dict(model_instance)

json.dumps(model_dict, cls=DjangoJSONEncoder)

Presto!

 3
Author: Javier Buzzi,
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-05-22 09:09:01

Basado en stdOrgnlDave respuesta He definido esta envoltura que se puede llamar con tipos opcionales para que el codificador funcione solo para ciertos tipos dentro de sus proyectos. Creo que el trabajo debe hacerse dentro de su código y no usar este codificador "predeterminado" ya que "es mejor explícito que implícito", pero entiendo que usar esto le ahorrará algo de tiempo. :-)

import time
import json
import decimal
from uuid import UUID
from datetime import datetime

def JSONEncoder_newdefault(kind=['uuid', 'datetime', 'time', 'decimal']):
    '''
    JSON Encoder newdfeault is a wrapper capable of encoding several kinds
    Use it anywhere on your code to make the full system to work with this defaults:
        JSONEncoder_newdefault()  # for everything
        JSONEncoder_newdefault(['decimal'])  # only for Decimal
    '''
    JSONEncoder_olddefault = json.JSONEncoder.default

    def JSONEncoder_wrapped(self, o):
        '''
        json.JSONEncoder.default = JSONEncoder_newdefault
        '''
        if ('uuid' in kind) and isinstance(o, uuid.UUID):
            return str(o)
        if ('datetime' in kind) and isinstance(o, datetime):
            return str(o)
        if ('time' in kind) and isinstance(o, time.struct_time):
            return datetime.fromtimestamp(time.mktime(o))
        if ('decimal' in kind) and isinstance(o, decimal.Decimal):
            return str(o)
        return JSONEncoder_olddefault(self, o)
    json.JSONEncoder.default = JSONEncoder_wrapped

# Example
if __name__ == '__main__':
    JSONEncoder_newdefault()
 1
Author: Juanmi Taboada,
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-17 11:12:42

Del Documento Estándar JSON , como se enlaza en json.org :

JSON es agnóstico sobre la semántica de los números. En cualquier lenguaje de programación, puede haber una variedad de tipos numéricos de diversas capacidades y complementos, fijos o flotantes, binarios o decimales. Que puede hacer el intercambio entre diferentes lenguajes de programación es difícil. JSON en su lugar ofrece solo la representación de números que los humanos usan: una secuencia de dígitos. Todos los lenguajes de programación saber cómo dar sentido a digit secuencias incluso si no están de acuerdo en las representaciones internas. Eso es suficiente para permitir el intercambio.

Así que en realidad es preciso representar decimales como números (en lugar de cadenas) en JSON. A continuación se encuentra una posible solución al problema.

Defina un codificador JSON personalizado:

import json


class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, Decimal):
            return float(obj)
        return super(CustomJsonEncoder, self).default(obj)

Luego utilícelo al serializar sus datos:

json.dumps(data, cls=CustomJsonEncoder)

Como se observa en los comentarios sobre las otras respuestas, las versiones anteriores de python podrían estropear la representación al convertir a float, pero ese ya no es el caso.

Para obtener ese decimal exacto en Python:

Decimal(str(value))

Esta solución se insinúa en la documentación de Python 3.0 sobre decimales :

Para crear un Decimal a partir de un flotador, primero conviértalo en una cadena.

 1
Author: hugo_leonardo,
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-04-08 01:51:04

Si desea pasar un diccionario que contenga decimales a la biblioteca requests (utilizando el argumento de la palabra clave json), simplemente necesita instalar simplejson:

$ pip3 install simplejson    
$ python3
>>> import requests
>>> from decimal import Decimal
>>> # This won't error out:
>>> requests.post('https://www.google.com', json={'foo': Decimal('1.23')})

La razón del problema es que requests usa simplejson solo si está presente, y vuelve a usar el incorporado json si no está instalado.

 0
Author: Max Malysh,
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-04-14 19:22:55

Puede crear un codificador JSON personalizado según sus requisitos.

import json
from datetime import datetime, date
from time import time, struct_time, mktime
import decimal

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return str(o)
        if isinstance(o, date):
            return str(o)
        if isinstance(o, decimal.Decimal):
            return float(o)
        if isinstance(o, struct_time):
            return datetime.fromtimestamp(mktime(o))
        # Any other serializer if needed
        return super(CustomJSONEncoder, self).default(o)

El Decodificador se puede llamar así,

import json
from decimal import Decimal
json.dumps({'x': Decimal('3.9')}, cls=CustomJSONEncoder)

Y la salida será:

>>'{"x": 3.9}'
 0
Author: sparrow,
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-09-06 11:47:09

Esto se puede hacer añadiendo

    elif isinstance(o, decimal.Decimal):
        yield str(o)

En \Lib\json\encoder.py:JSONEncoder._iterencode, pero esperaba una mejor solución

 -4
Author: Knio,
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
2009-12-25 05:37:41