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.



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)

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.


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)dá '{"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.


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


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

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!


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 ...


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)

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)

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.


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)

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

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

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

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.


-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


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.