Python JSON serializa un objeto decimal


242

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

¿Hay una buena manera de serializar esto? JSONDecoder no acepta objetos decimales, y la conversión a un flotante de antemano produce lo {'x': 3.8999999999999999}que está mal, y será un gran desperdicio de ancho de banda.


2
error relacionado de Python: el codificador json no puede manejar el decimal
jfs

3.8999999999999999 no está más equivocado que 3.4. 0.2 no tiene representación flotante exacta.
Jasen

@Jasen 3.89999999999 es aproximadamente un 12.8% más incorrecto que 3.4. El estándar JSON solo se trata de serialización y notación, no de implementación. El uso de IEEE754 no es parte de la especificación JSON sin procesar, es solo la forma más común de implementarlo. Una implementación que usa solo aritmética decimal precisa es completamente (de hecho, incluso más estrictamente) conforme.
hraban

😂 menos mal. irónico.
hraban

Respuestas:


147

¿Qué tal subclases 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)

Luego úsalo así:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)

Ouch, acabo de notar que en realidad no funcionará así. Se editará en consecuencia. (Sin embargo, la idea sigue siendo la misma.)
Michał Marczyk

El problema fue que DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()devolvió el correcto '3.9', pero DecimalEncoder()._iterencode(3.9).next()devolvió un objeto generador que solo regresaría '3.899...'cuando se acumulaba en otro .next(). Generador de negocios divertidos. Oh bueno ... Debería funcionar ahora.
Michał Marczyk

8
¿No puedes simplemente en su return (str(o),)lugar? [o]es una lista con solo 1 elemento, ¿por qué molestarse en recorrerlo?
mpen

2
@Mark: return (str(o),)devolvería una tupla de longitud 1, mientras que el código en la respuesta devuelve el generador de longitud 1. Ver los documentos iterencode ()
Abgan

30
Esta implementación ya no funciona. El de Elias Zamaria es el que trabaja en el mismo estilo.
piro

224

Simplejson 2.1 y superior tiene soporte nativo para tipo Decimal:

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

Tenga en cuenta que use_decimales Truepor 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):

Entonces:

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

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


77
Hmm, para mí esto convierte objetos decimales en flotantes, lo cual no es aceptable. Pérdida de precisión al trabajar con divisas, por ejemplo.
Matthew Schinckel

12
@MatthewSchinckel Creo que no. Realmente hace una cadena de ella. Y si alimenta la cadena resultante de nuevo json.loads(s, use_decimal=True), le devuelve el decimal. Sin flotación en todo el proceso. Editado arriba de la respuesta. Espero que el póster original esté bien con él.
Shekhar

1
Ajá, creo que tampoco lo estaba usando use_decimal=Trueen las cargas.
Matthew Schinckel

1
Para mi json.dumps({'a' : Decimal('3.9')}, use_decimal=True)da '{"a": 3.9}'. ¿No fue el gol '{"a": "3.9"}'?
MrJ

55
simplejson.dumps(decimal.Decimal('2.2'))también funciona: no explícito use_decimal(probado en simplejson / 3.6.0). Otra forma de volver a cargarlo es: es json.loads(s, parse_float=Decimal)decir, puede leerlo usando stdlib json(y simplejsontambién se admiten versiones anteriores).
jfs

181

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 Decimal y esto es lo que se me ocurrió:

import decimal

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

Con suerte, esto debería ayudar a cualquiera que tenga problemas con Python 2.7. Lo probé y parece funcionar bien. Si alguien nota algún error en mi solución o encuentra una mejor manera, hágamelo saber.


44
Python 2.7 cambió las reglas para redondear flotadores para que esto funcione. Ver discusión en stackoverflow.com/questions/1447287/…
Nelson

2
Para aquellos de nosotros que no podemos usar simplejson (es decir, en Google App Engine), esta respuesta es un regalo del cielo.
Joel Cross

17
Use unicodeo en strlugar de floatpara garantizar la precisión.
Seppo Erviälä

2
El problema con 54.3999 ... era importante en Python 2.6.xy anteriores, donde la conversión flotante a cadena no funcionaba regularmente, pero la conversión Decimal a cadena es mucho más incorrecta porque se serializaría como cadena con comillas dobles "54.4", no como un número.
hynekcer

1
Funciona en python3
SeanFromIT

43

En mi aplicación Flask, que usa python 2.7.11, matraz alquimia (con tipos 'db.decimal') y Flask Marshmallow (para serializador y deserializador 'instantáneo'), tuve este error, cada vez que hice un GET o POST . El serializador y el deserializador no pudieron convertir los tipos decimales en ningún formato identificable JSON.

Hice un "pip install simplejson", luego simplemente agregando

import simplejson as json

el serializador y el deserializador comienzan a ronronear nuevamente. No hice nada más ... DEciamls se muestran como formato flotante '234.00'.


1
la solución más fácil
SMDC

1
Por extraño que parezca, ni siquiera tiene que importar simplejson, solo instalarlo hace el truco. Inicialmente mencionado por esta respuesta .
bsplosion

Esto no funciona en mí, y todavía lo tengo Decimal('0.00') is not JSON serializable después de instalarlo a través de pip. Esta situación es cuando estás usando malvavisco y grafeno. Cuando se llama a una consulta en una API de reposo, malvavisco funciona de forma esperada para los campos decimales. Sin embargo, cuando se llama con graphql, genera un is not JSON serializableerror.
Roel

Fantástico, excelente
Spiderman

¡Perfecto! Esto funciona en situaciones en las que está utilizando un módulo escrito por otra persona que no puede modificar fácilmente (en mi caso, gspread para usar Hojas de cálculo de Google)
happyskeptic

32

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

Si el valor predeterminado devuelve un objeto de una clase que hereda de float (o cualquier cosa que llame a repr sin formato adicional) y tenga 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]'

¡Agradable! Esto asegura que el valor decimal termine en el JSON como un flotante Javascript, sin que Python lo redondee primero al valor flotante más cercano.
konrad

3
Lamentablemente, esto no funciona en Python 3 recientes. Ahora hay un código de ruta rápida que considera todas las subclases flotantes como flotantes, y no llama a repr por completo.
Antti Haapala

@AnttiHaapala, el ejemplo funciona bien en Python 3.6.
Cristian Ciupitu

@CristianCiupitu, de hecho, no parece que pueda reproducir el mal comportamiento ahora
Antti Haapala

2
La solución dejó de funcionar desde v3.5.2rc1, consulte github.com/python/cpython/commit/… . Hay un float.__repr__código rígido (que pierde precisión) y fakefloat.__repr__no se llama en absoluto. La solución anterior funciona correctamente para python3 hasta 3.5.1, si fakefloat tiene un método adicional def __float__(self): return self.
myroslav

30

Falta la opción nativa, así que la agregaré para la próxima persona que la busque.

A partir de Django 1.7.x hay una función integrada desde la DjangoJSONEncoderque puede obtenerla 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!


Aunque es bueno saberlo, ¿el OP no preguntó por Django?
std''OrgnlDave

44
@ std''OrgnlDave estás 100% correcto. Olvidé cómo llegué aquí, pero busqué en Google esta pregunta con "django" adjunto al término de búsqueda y surgió, después de buscar un poco más en Google, encontré la respuesta y la agregué aquí para la siguiente persona como yo, que tropieza. it
Javier Buzzi

66
me salvas el día
gaozhidf

14

¡Mis $ .02!

Extiendo un montón del codificador JSON ya que estoy serializando toneladas de datos para mi servidor web. Aquí hay un buen código. Tenga en cuenta que es fácilmente extensible a casi cualquier formato de datos que desee y se 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 ...


3
Esto es incorrecto: reproducirá 3.9 como "thing": "3.9".
Glifo

la mejor solución de todas, muy simple, gracias por salvarme el día, para mí es suficiente para guardar el número, en cadena para decimal está bien
stackdave

@Glyph a través de los estándares JSON (de los cuales hay algunos ...), un número sin comillas es un punto flotante de doble precisión, no un número decimal. Citarlo es la única forma de garantizar la compatibilidad.
std''OrgnlDave

2
¿Tienes una cita para esto? Cada especificación que he leído implica que depende de la implementación.
Glifo

12

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

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

Entonces, si no desea la opción flotante, solo tiene que enviarla como una cadena y permitir la conversión automática de objetos decimales a JSON, haga 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)

Sé que no será 3.9 internamente una vez que se analice en el cliente, pero 3.9 es un flotante JSON válido. es decir, json.loads("3.9")funcionará, y me gustaría que fuera así
Knio

@Anurag Usted quiso decir repr (obj) en lugar de repr (o) en su ejemplo.
orokusaki

¿Esto no morirá si intentas codificar algo que no sea decimal?
mikemaccana

1
@nailer, no, no lo harás, puedes intentarlo, la razón es la excepción de aumento predeterminada para indicar que se debe usar el siguiente controlador
Anurag Uniyal

1
Vea la respuesta de mikez302: en Python 2.7 o superior, esto ya no se aplica.
Joel Cross

9

Para usuarios de Django :

Recientemente me encontré con la TypeError: Decimal('2337.00') is not JSON serializable codificación JSON, es decirjson.dumps(data)

Solución :

# converts Decimal, Datetime, UUIDs to str for Encoding
from django.core.serializers.json import DjangoJSONEncoder  

json.dumps(response.data, cls=DjangoJSONEncoder)

Pero, ahora el valor decimal será una cadena, ahora podemos establecer explícitamente el analizador de valor decimal / flotante al decodificar datos, usando la parse_floatopción en json.loads:

import decimal 

data = json.loads(data, parse_float=decimal.Decimal) # default is float(num_str)

8

Del documento estándar JSON , como se vincula 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 de números de varias capacidades y complementos, fijos o flotantes, binarios o decimales. Eso puede dificultar el intercambio entre diferentes lenguajes de programación. En cambio, JSON ofrece solo la representación de números que usan los humanos: una secuencia de dígitos. Todos los lenguajes de programación saben cómo dar sentido a las secuencias de dígitos, incluso si no están de acuerdo con las representaciones internas. Eso es suficiente para permitir el intercambio.

Por lo tanto, en realidad es preciso representar los 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 úselo cuando serialice sus datos:

json.dumps(data, cls=CustomJsonEncoder)

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

Para recuperar el decimal 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.


2
Esto no está "arreglado" en Python 3. La conversión a un float necesariamente te hace perder la representación decimal, y dará lugar a discrepancias. Si Decimales importante usarlo, creo que es mejor usar cadenas.
juanpa.arrivillaga

Creo que es seguro hacerlo desde Python 3.1. La pérdida de precisión puede ser dañina en las operaciones aritméticas, pero en el caso de la codificación JSON, simplemente está produciendo una visualización de cadena del valor, por lo que la precisión es más que suficiente para la mayoría de los casos de uso. Todo en JSON ya es una cadena, por lo que poner comillas alrededor del valor solo desafía la especificación JSON.
Hugo Mota

Dicho esto, entiendo las preocupaciones sobre la conversión a flotador. Probablemente haya una estrategia diferente para usar con el codificador para producir la cadena de visualización deseada. Aún así, no creo que valga la pena producir un valor cotizado.
Hugo Mota

@HugoMota "Todo en JSON ya es una cadena, por lo que poner comillas alrededor del valor simplemente desafía la especificación JSON". No: rfc-editor.org/rfc/rfc8259.txt : JSON es un formato de codificación basado en texto, pero eso no significa que todo en él deba interpretarse como una cadena. La especificación define cómo codificar números, por separado de las cadenas.
Gunnar Þór Magnússon

@ GunnarÞórMagnússon "JSON es un formato de codificación basado en texto", eso es lo que quise decir con "todo es una cadena". Convertir los números a una cadena de antemano no preservará mágicamente la precisión, ya que de todos modos será una cadena cuando se convierta en JSON. Y de acuerdo con la especificación, los números no tienen comillas alrededor. Es responsabilidad del lector preservar la precisión durante la lectura (no una cita, solo mi opinión al respecto).
Hugo Mota

6

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)

json.loads(myString, cls=CommonJSONEncoder)comentario debería serjson.loads(myString, cls=CommonJSONDecoder)
¿Puede Kavaklıoğlu el

object_hook necesita un valor de retorno predeterminado si obj no es decimal.
¿Puede Kavaklıoğlu

3

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

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}'

increíble ... Gracias por una solución integral (y)
muhammed basil

Esto realmente funciona! Gracias por compartir su solución
tthreetorch

3

Para aquellos que no quieren usar una biblioteca de terceros ... Un problema con la respuesta de Elias Zamaria es que se convierte en flotante, lo que puede generar problemas. Por ejemplo:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 1e-07}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01733}'

El JSONEncoder.encode()método le permite devolver el contenido literal de json, a diferencia de lo JSONEncoder.default()que le permite devolver un tipo compatible con json (como flotante) que luego se codifica de la manera normal. El problema encode()es que (normalmente) solo funciona en el nivel superior. Pero aún es utilizable, con un poco de trabajo extra (python 3.x):

import json
from collections.abc import Mapping, Iterable
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
    def encode(self, obj):
        if isinstance(obj, Mapping):
            return '{' + ', '.join(f'{self.encode(k)}: {self.encode(v)}' for (k, v) in obj.items()) + '}'
        if isinstance(obj, Iterable) and (not isinstance(obj, str)):
            return '[' + ', '.join(map(self.encode, obj)) + ']'
        if isinstance(obj, Decimal):
            return f'{obj.normalize():f}'  # using normalize() gets rid of trailing 0s, using ':f' prevents scientific notation
        return super().encode(obj)

Lo que te da:

>>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
'{"x": 0.0000001}'
>>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
'{"x": 100000000000.01734}'

2

Basado en la respuesta de stdOrgnlDave , he definido este contenedor 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()

0

Si desea pasar un diccionario que contiene decimales a la requestsbiblioteca (utilizando el jsonargumento de la palabra clave), 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 solo se requestsusa simplejsonsi está presente, y recurre al incorporado jsonsi no está instalado.


-6

esto se puede hacer agregando

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

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


55
Puede subclasificar JSONEncoder como se muestra arriba, editar los archivos Python instalados de una biblioteca establecida o el propio intérprete debería ser el último recurso.
justanr
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.