JSON Python serializa um objeto decimal

242

Eu tenho um Decimal('3.9')como parte de um objeto, e gostaria de codificar isso para uma string JSON que deve se parecer {'x': 3.9}. Eu não me importo com precisão no lado do cliente, então um float é bom.

Existe uma boa maneira de serializar isso? JSONDecoder não aceita objetos decimais, e a conversão para um float anteriormente gera o {'x': 3.8999999999999999}que está errado e será um grande desperdício de largura de banda.

Knio
fonte
2
bug Python relacionado: codificador json incapaz de manipular decimal
jfs
3.8999999999999999 não está mais errado do que 3.4. 0.2 não tem representação exata de flutuação.
Jasen
@Jasen 3.89999999999 é cerca de 12,8% mais errado que 3,4. O padrão JSON é apenas sobre serialização e notação, não implementação. O uso do IEEE754 não faz parte da especificação JSON bruta, é apenas a maneira mais comum de implementá-lo. Uma implementação que usa apenas aritmética decimal precisa é completamente (de fato, ainda mais estritamente) conforme.
hraban
😂 menos errado. irônico.
hraban

Respostas:

147

Que tal subclassificar 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)

Em seguida, use-o assim:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
Michał Marczyk
fonte
Ai, acabei de perceber que não vai funcionar assim. Editará de acordo. (A idéia permanece a mesma, no entanto.)
Michał Marczyk
O problema foi que DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()retornou o correto '3.9', mas DecimalEncoder()._iterencode(3.9).next()retornou um objeto gerador que só retornaria '3.899...'quando você empilhasse em outro .next(). Engraçado negócio de gerador. Oh bem ... Deve funcionar agora.
Michał Marczyk
8
Você não pode apenas em return (str(o),)vez disso? [o]é uma lista com apenas 1 elemento, por que se preocupar em repetir isso?
MPEN
2
@ Marcos: return (str(o),)voltaria tupla de comprimento 1, enquanto o código na resposta retornos gerador de comprimento 1. Veja a iterencode () docs
Abgan
30
Esta implementação não funciona mais. O de Elias Zamaria é aquele que trabalha no mesmo estilo.
Piro
223

O Simplejson 2.1 e superior tem suporte nativo para o tipo Decimal:

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

Observe que isso use_decimalé Truepor padrão:

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):

Assim:

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

Felizmente, esse recurso será incluído na biblioteca padrão.

Lukas Cenovsky
fonte
7
Hmm, para mim isso converte objetos decimais em carros alegóricos, o que não é aceitável. Perda de precisão ao trabalhar com moeda, por exemplo.
Matthew Schinckel 22/10/10
12
@MatthewSchinckel Eu acho que não. Na verdade, ele cria uma corda. E se você alimentar a string resultante, json.loads(s, use_decimal=True)ela retornará o decimal. Nenhuma flutuação em todo o processo. Editado acima da resposta. O cartaz original da esperança é bom com ele.
Shekhar #
1
Ah, acho que também não estava usando use_decimal=Trueas cargas.
Matthew Schinckel
1
Para mim json.dumps({'a' : Decimal('3.9')}, use_decimal=True)'{"a": 3.9}'. O objetivo não era '{"a": "3.9"}'?
MrJ
5
simplejson.dumps(decimal.Decimal('2.2'))também funciona: não explícito use_decimal(testado no simplejson / 3.6.0). Outra maneira de carregá-lo de volta é: json.loads(s, parse_float=Decimal)ou seja, você pode lê-lo usando stdlib json(e simplejsonversões antigas também são suportadas).
JFS
181

Gostaria de informar a todos que tentei a resposta de Michał Marczyk no meu servidor Web que estava executando o Python 2.6.5 e funcionou bem. No entanto, atualizei para o Python 2.7 e ele parou de funcionar. Eu tentei pensar em algum tipo de maneira de codificar objetos decimais e é isso que eu criei:

import decimal

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

Espero que isso ajude quem está tendo problemas com o Python 2.7. Eu testei e parece funcionar bem. Se alguém perceber algum erro na minha solução ou apresentar uma maneira melhor, informe-me.

Elias Zamaria
fonte
4
O Python 2.7 mudou as regras para arredondar carros alegóricos, então isso funciona. Veja a discussão em stackoverflow.com/questions/1447287/…
Nelson
2
Para aqueles de nós que não podem usar o simplejson (ou seja, no Google App Engine), essa resposta é uma dádiva de Deus.
Joel Cross
17
Use unicodeou em strvez de floatpara garantir precisão.
Seppo Erviälä
2
O problema com 54.3999 ... era importante no Python 2.6.xe mais antigo, onde a conversão de flutuação para string não funcionava regularmente, mas a conversão de decimal para str é muito mais incorreta, pois seria serializada como string com aspas duplas "54.4", não como um número.
hynekcer
1
Funciona em python3
SeanFromIT
43

No meu aplicativo Flask, que usa python 2.7.11, alquimia de frascos (com tipos 'db.decimal') e Flask Marshmallow (para serializador e desserializador 'instantâneos'), eu tinha esse erro sempre que fazia um GET ou POST . O serializador e o desserializador falharam ao converter tipos decimais em qualquer formato identificável JSON.

Eu fiz um "pip install simplejson" e, em seguida, apenas adicionando

import simplejson as json

o serializador e o desserializador começam a ronronar novamente. Eu não fiz mais nada ... DEciamls são exibidos como formato flutuante '234.00'.

ISONecroMAn
fonte
1
a correção mais fácil
SMDC
1
Curiosamente, você nem precisa importar simplejson- basta instalá-lo. Mencionado inicialmente por esta resposta .
precisa saber é
Isso não funciona comigo, e ainda assim Decimal('0.00') is not JSON serializable após a instalação via pip. Esta situação é quando você está usando marshmallow e grafeno. Quando uma consulta é chamada em uma API restante, o marshmallow funciona de maneira esperada para campos decimais. No entanto, quando é chamado com graphql, gera um is not JSON serializableerro.
Roel
Fantástico, Soberbo,
Homem
Perfeito! Isso funciona em situações onde você está usando um módulo escrito por outra pessoa whcih você não pode modificar facilmente (no meu caso gspread para a utilização de planilhas do Google)
happyskeptic
32

Tentei mudar do simplejson para o json embutido no GAE 2.7 e tive problemas com o decimal. Se o padrão retornasse str (o), havia aspas (porque _iterencode chama _iterencode nos resultados do padrão) e float (o) removeria o zero final.

Se o padrão retornar um objeto de uma classe que herda de float (ou qualquer coisa que chame repr sem formatação adicional) e tenha um método __repr__ personalizado, ele parece funcionar como eu quero.

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]'
tesdal
fonte
Agradável! Isso garante que o valor decimal seja finalizado no JSON como um flutuador Javascript, sem que o Python faça o primeiro arredondamento para o valor flutuante mais próximo.
Konrad
3
Infelizmente, isso não funciona nos recentes Python 3. Agora existe um código de atalho que considera todas as subclasses de flutuação como flutuações e não as chama totalmente de repr.
Antti Haapala
@AnttiHaapala, o exemplo funciona bem no Python 3.6.
Cristian Ciupitu
@CristianCiupitu, de fato, não pareço ser capaz de reproduzir o mau comportamento agora
Antti Haapala:
2
A solução parou de funcionar desde a v3.5.2rc1, consulte github.com/python/cpython/commit/… . Não é float.__repr__codificado (que perde a precisão), e fakefloat.__repr__não é chamado de todo. A solução acima funciona corretamente para python3 até 3.5.1, se fakefloat tiver um método adicional def __float__(self): return self.
Myroslav
30

A opção nativa está faltando, então eu a adicionarei para o próximo cara / galha que a procurar.

A partir do Django 1.7.x, há um built-in do DjangoJSONEncoderqual você pode obtê-lo 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!

Javier Buzzi
fonte
Embora seja bom saber disso, o OP não perguntou sobre o Django?
std''OrgnlDave
4
@ std''OrgnlDave você está 100% correto. Eu esqueci como cheguei aqui, mas pesquisei esta questão com "django" anexado ao termo de pesquisa e surgiu, depois de um pouco mais de pesquisa, encontrei a resposta e a adicionei aqui para a próxima pessoa como eu, que tropeça -lo
Javier Buzzi
6
você salva o meu dia #
1929 gaozhidf
14

Meu $ .02!

Eu estendo um monte de codificador JSON desde que estou serializando toneladas de dados para o meu servidor web. Aqui está um bom código. Observe que é facilmente extensível a praticamente qualquer formato de dados que você desejar e reproduz 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

Torna minha vida muito mais fácil ...

std''OrgnlDave
fonte
3
Isso está incorreto: ele reproduzirá 3,9 como "thing": "3.9".
Glyph
as melhores soluções de tudo, muito simples, graças você salvou o meu dia, para mim é o suficiente para salvar o número em string para decimal é ok
stackdave
@Glyph via padrões JSON (dos quais existem alguns ...), um número não citado é um ponto flutuante de precisão dupla, não um número decimal. Citar é a única maneira de garantir compatibilidade.
usar o seguinte comando
2
você tem uma citação para isso? Cada especificação que eu li implica que é dependente da implementação.
Glyph
12

3.9não pode ser representado exatamente nos carros alegóricos IEEE, sempre virá como 3.8999999999999999, por exemplo print repr(3.9), tente , você pode ler mais sobre isso aqui:

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

Portanto, se você não quiser flutuar, apenas a opção deve ser enviada como string e para permitir a conversão automática de objetos decimais em JSON, faça algo assim:

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)
Anurag Uniyal
fonte
Eu sei que não será o 3.9 internamente, uma vez analisado no cliente, mas o 3.9 é um float JSON válido. ou seja, json.loads("3.9")vai funcionar, e eu gostaria que fosse isso #
Knio 25/12/2009
@Anurag Você quis dizer repr (obj) em vez de repr (o) no seu exemplo.
Orokusaki
Isso não vai morrer se você tentar codificar algo que não é decimal?
Mikemaccana 5/08
1
@nailer, não, não, você pode tentar isso, razão de ser exceção raise padrão para sinal de que no próximo manipulador deve ser usado
Anurag Uniyal
1
Veja a resposta de mikez302 - no Python 2.7 ou superior, isso não se aplica mais.
Joel Cross
9

Para usuários do Django :

Recentemente, deparei com a TypeError: Decimal('2337.00') is not JSON serializable codificação JSON, ou seja,json.dumps(data)

Solução :

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

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

Mas agora o valor decimal será uma string, agora podemos definir explicitamente o analisador de valor decimal / flutuante ao decodificar dados, usando a parse_floatopção em json.loads:

import decimal 

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

No documento padrão JSON , conforme vinculado em json.org :

JSON é independente da semântica dos números. Em qualquer linguagem de programação, pode haver vários tipos de números de várias capacidades e complementos, fixos ou flutuantes, binários ou decimais. Isso pode dificultar o intercâmbio entre diferentes linguagens de programação. Em vez disso, o JSON oferece apenas a representação dos números que os humanos usam: uma sequência de dígitos. Todas as linguagens de programação sabem como entender seqüências de dígitos, mesmo que discordem das representações internas. Isso é suficiente para permitir o intercâmbio.

Portanto, é realmente preciso representar decimais como números (em vez de cadeias) em JSON. Abaixo está uma possível solução para o problema.

Defina um codificador JSON customizado:

import json


class CustomJsonEncoder(json.JSONEncoder):

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

Em seguida, use-o ao serializar seus dados:

json.dumps(data, cls=CustomJsonEncoder)

Conforme observado nos comentários sobre as outras respostas, as versões mais antigas do python podem atrapalhar a representação ao converter para float, mas esse não é mais o caso.

Para recuperar o decimal em Python:

Decimal(str(value))

Esta solução é sugerida na documentação do Python 3.0 em decimais :

Para criar um decimal de um flutuador, primeiro converta-o em uma string.

Hugo Mota
fonte
2
Isto não é "fixo" em Python 3. A conversão para um float necessariamente faz você perder a representação decimal, e vai levar a discrepâncias. Se Decimalé importante usar, acho melhor usar strings.
Juanpa.arrivillaga 01/01/19
Eu acredito que é seguro fazer isso desde python 3.1. A perda de precisão pode ser prejudicial em operações aritméticas, mas no caso da codificação JSON, você está apenas produzindo uma exibição de sequência do valor, portanto, a precisão é mais que suficiente para a maioria dos casos de uso. Tudo no JSON já é uma sequência, portanto, colocar aspas ao redor do valor apenas desafia a especificação JSON.
Hugo Mota
Com isso dito, entendo as preocupações em torno da conversão para flutuar. Provavelmente existe uma estratégia diferente para usar com o codificador para produzir a sequência de exibição desejada. Ainda assim, acho que não vale a pena produzir um valor citado.
Hugo Mota
@HugoMota "Tudo no JSON já é uma string, então colocar aspas ao redor do valor apenas desafia a especificação JSON." Não: rfc-editor.org/rfc/rfc8259.txt - JSON é um formato de codificação baseado em texto, mas isso não significa que tudo nele deve ser interpretado como uma sequência. A especificação define como codificar números, separadamente das strings.
Gunnar Magnór Magnússon
@ GunnarÞórMagnússon "JSON é um formato de codificação baseado em texto" - foi o que eu quis dizer com "tudo é uma string". A conversão antecipada dos números em string não preservará magicamente a precisão, pois será uma string quando se tornar JSON. E de acordo com a especificação, os números não têm aspas. É responsabilidade do leitor preservar a precisão durante a leitura (não uma citação, apenas minha opinião).
Hugo Mota
6

Isto é o que eu tenho, extraído da nossa classe

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 passa mais 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)
James Lin
fonte
json.loads(myString, cls=CommonJSONEncoder)comentário deve serjson.loads(myString, cls=CommonJSONDecoder)
Pode Kavaklıoğlu
object_hook precisa de um valor de retorno padrão se obj não for decimal.
Pode Kavaklıoğlu
3

Você pode criar um codificador JSON customizado conforme sua exigência.

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)

O decodificador pode ser chamado assim,

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

e a saída será:

>>'{"x": 3.9}'
pardal
fonte
incrível ... Obrigado por uma solução de parada (y)
muhammed manjericão
Isso realmente funciona! Obrigado por compartilhar sua solução
tthreetorch
3

Para aqueles que não querem usar uma biblioteca de terceiros ... Um problema com a resposta de Elias Zamaria é que ele se converte em flutuante, o que pode gerar problemas. Por exemplo:

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

O JSONEncoder.encode()método permite retornar o conteúdo literal do json, ao contrário do JSONEncoder.default()que você devolve um tipo compatível com o json (como float) que é codificado da maneira normal. O problema encode()é que (normalmente) funciona apenas no nível superior. Mas ainda é utilizável, com um pouco de trabalho 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)

O que lhe dá:

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

Com base na resposta stdOrgnlDave , defini este wrapper que pode ser chamado com tipos opcionais, para que o codificador funcione apenas para certos tipos dentro de seus projetos. Eu acredito que o trabalho deve ser feito dentro do seu código e não para usar esse codificador "padrão", pois "é melhor explícito do que implícito", mas eu entendo que isso poupará seu tempo. :-)

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()
Juanmi Taboada
fonte
0

Se você deseja passar um dicionário contendo casas decimais para a requestsbiblioteca (usando o jsonargumento de palavra - chave), basta 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')})

O motivo do problema é que ele é requestsusado simplejsonapenas se estiver presente e volta ao incorporado jsonse não estiver instalado.

Max Malysh
fonte
-6

isso pode ser feito adicionando

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

no \Lib\json\encoder.py:JSONEncoder._iterencode, mas eu estava esperando por uma solução melhor

Knio
fonte
5
Você pode subclassificar JSONEncoder como examinado acima, editar os arquivos Python instalados de uma biblioteca estabelecida ou o próprio intérprete deve ser o último recurso.
justanr