Python JSON sérialize un objet décimal

j'ai un Decimal('3.9') comme partie d'un objet, et je souhaite l'encoder à une chaîne JSON qui devrait ressembler à {'x': 3.9} . Je me fiche de la précision côté client, donc un flotteur est parfait.

y a-t-il un bon moyen de sérialiser cela? JSONDecoder n'accepte pas les objets décimaux, et la conversion à un flotteur à l'avance donne {'x': 3.8999999999999999} ce qui est faux, et sera un grand gaspillage de bande passante.

168
demandé sur Knio 2009-12-25 08:00:12

14 réponses

Que Diriez-vous de sous-classer 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)

alors utilisez - le comme ceci:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
110
répondu Michał Marczyk 2009-12-25 07:14:05

Simplejson 2.1 et plus a le support natif pour le type décimal:

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

notez que use_decimal est True par défaut:

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

:

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

avec un peu de chance, cette fonctionnalité sera incluse dans la bibliothèque standard.

167
répondu Lukas Cenovsky 2017-02-23 16:36:38

je voudrais faire savoir à tout le monde que j'ai essayé la réponse de Michał Marczyk sur mon serveur web qui exécutait Python 2.6.5 et ça a bien fonctionné. Cependant, je suis passé à Python 2.7 et il a cessé de fonctionner. J'ai essayé de penser à une sorte de façon D'encoder des objets décimaux et c'est ce que j'ai trouvé:

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

cela devrait aider quiconque a des problèmes avec Python 2.7. Je l'ai testé et il semble bien fonctionner. Si quelqu'un remarque des bugs dans mon solution ou est livré avec une meilleure manière, s'il vous plaît laissez-moi savoir.

122
répondu Elias Zamaria 2010-10-07 19:29:03

j'ai essayé de passer de simplejson à builtin json pour GAE 2.7, et avait des problèmes avec la décimale. Si default renvoie str (o), il y a des guillemets(parce que _iterencode appelle _iterencode sur les résultats de default), et float (o) supprimera le tracing 0.

si par défaut renvoie un objet d'une classe qui hérite de float (ou quelque chose qui appelle repr sans formatage supplémentaire) et a une méthode __repr__ personnalisée, il semble fonctionner comme je le veux.

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
répondu tesdal 2011-11-25 21:17:24

dans mon application Flask, qui utilise python 2.7.11, flask alchemy (avec 'db.décimal ' types), et Flask Marshmallow ( pour 'instant' serializer et deserializer), j'ai eu cette erreur, chaque fois que j'ai fait un GET ou POST. Le serializer et deserializer, n'a pas réussi à convertir les types décimaux dans n'importe quel format identifiable JSON.

j'ai fait un "pip install simplejson", puis Juste en ajoutant

import simplejson as json

le serializer et deserializer recommence à ronronner. Je n'a rien fait d'autre... Les DEciamls sont affichés en format float '234.00'.

18
répondu ISONecroMAn 2016-08-31 19:21:54

3.9 ne peut pas être représenté exactement dans les flotteurs IEEE, il viendra toujours comme 3.8999999999999999 , par exemple essayer print repr(3.9) , vous pouvez lire plus à ce sujet ici:

http://en.wikipedia.org/wiki/Floating_point

http://docs.sun.com/source/806-3568/ncg_goldberg.html

donc si vous ne voulez pas de float, seule option vous devez l'envoyer en string, et pour permettre conversion automatique d'objets décimaux en JSON, faire quelque chose comme ceci:

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
répondu Anurag Uniyal 2014-08-28 16:39:05

mon $.02!

j'étends un tas de l'encodeur JSON puisque je sérialise des tonnes de données pour mon serveur web. Voici un peu de bon code. Notez qu'il est facilement extensible à peu près n'importe quel format de données que vous vous sentez et va reproduire 3.9 comme "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

rend ma vie tellement plus facile...

8
répondu std''OrgnlDave 2017-03-27 05:35:13

C'est ce que j'ai, extrait de notre 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)

qui passe 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
répondu James Lin 2015-02-02 01:30:54

l'option native est manquante donc je vais l'Ajouter pour le prochain gars/Galle qui le cherche.

à partir de Django 1.7.x il y a un DjangoJSONEncoder intégré que vous pouvez obtenir 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
répondu Javier Buzzi 2018-05-22 09:09:01

Basé sur stdOrgnlDave réponse j'ai défini ce wrapper qu'il peut être appelé avec l'option types de sorte que le codeur fonctionne uniquement pour certains types à l'intérieur de vos projets. Je pense que le travail devrait être fait à l'intérieur de votre code et de ne pas utiliser cet encodeur "par défaut" car "c'est mieux explicite qu'implicite", mais je comprends que l'utilisation de ce code vous fera gagner du temps. :- )

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
répondu Juanmi Taboada 2018-01-17 11:12:42

De la JSON Document Standard , lié à json.org :

JSON est agnostique sur la sémantique des nombres. Dans tout langage de programmation, il peut être une variété de nombre types de différentes capacités et compléments, fixes ou flottants, binaires ou décimaux. Que peut faire échange entre différents langages de programmation difficile. JSON offre plutôt seulement la représentation de nombre que les humains utilisent: une séquence de chiffres. Tous les langages de programmation savent donner un sens à digit même s'ils ne sont pas d'accord sur les représentations internes. Qui est suffisant pour permettre l'échange.

donc il est en fait exact de représenter des décimales en tant que nombres (plutôt que des chaînes) dans JSON. Soufflet se trouve une solution possible au problème.

Définir un JSON personnalisé encodeur:

import json


class CustomJsonEncoder(json.JSONEncoder):

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

alors utilisez-le quand sérialiser vos données:

json.dumps(data, cls=CustomJsonEncoder)

comme noté dans les commentaires sur les autres réponses, les versions plus anciennes de python pourraient gâcher la représentation lors de la conversion en float, mais ce n'est plus le cas.

pour obtenir ce retour décimal exact en Python:

Decimal(str(value))

cette solution est suggérée dans Python 3.0 documentation sur les décimales :

pour créer une décimale de un char, convertissez-le d'abord en chaîne.

1
répondu hugo_leonardo 2018-04-08 01:51:04

si vous voulez passer un dictionnaire contenant des décimales à la bibliothèque requests (en utilisant l'argument de mot-clé json ), vous avez simplement besoin d'installer 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 raison du problème est que requests utilise simplejson seulement si elle est présente, et retombe à la json intégrée si elle n'est pas installée.

0
répondu Max Malysh 2018-04-14 19:22:55

vous pouvez créer un encodeur JSON personnalisé selon vos besoins.

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)

le décodeur peut être appelé comme ceci,

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

et la sortie sera:

>>'{"x": 3.9}'
0
répondu sparrow 2018-09-06 11:47:09

cela peut être fait en ajoutant

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

dans \Lib\json\encoder.py:JSONEncoder._iterencode , mais j'espérais une meilleure solution

-4
répondu Knio 2009-12-25 05:37:41