Python JSON serialize a Decimal object

后端 未结 17 1172
星月不相逢
星月不相逢 2020-11-22 08:27

I have a Decimal(\'3.9\') as part of an object, and wish to encode this to a JSON string which should look like {\'x\': 3.9}. I don\'t care about p

相关标签:
17条回答
  • 2020-11-22 09:11

    For those who don't want to use a third-party library... An issue with Elias Zamaria's answer is that it converts to float, which can run into problems. For example:

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

    The JSONEncoder.encode() method lets you return the literal json content, unlike JSONEncoder.default(), which has you return a json compatible type (like float) that then gets encoded in the normal way. The problem with encode() is that it (normally) only works at the top level. But it's still usable, with a little extra work (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)
    

    Which gives you:

    >>> json.dumps({'x': Decimal('0.0000001')}, cls=DecimalEncoder)
    '{"x": 0.0000001}'
    >>> json.dumps({'x': Decimal('100000000000.01734')}, cls=DecimalEncoder)
    '{"x": 100000000000.01734}'
    
    0 讨论(0)
  • 2020-11-22 09:15

    3.9 can not be exactly represented in IEEE floats, it will always come as 3.8999999999999999, e.g. try print repr(3.9), you can read more about it here:

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

    So if you don't want float, only option you have to send it as string, and to allow automatic conversion of decimal objects to JSON, do something like this:

    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)
    
    0 讨论(0)
  • 2020-11-22 09:21

    If you want to pass a dictionary containing decimals to the requests library (using the json keyword argument), you simply need to install 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')})
    

    The reason of the problem is that requests uses simplejson only if it is present, and falls back to the built-in json if it is not installed.

    0 讨论(0)
  • 2020-11-22 09:22

    In my Flask app, Which uses python 2.7.11, flask alchemy(with 'db.decimal' types), and Flask Marshmallow ( for 'instant' serializer and deserializer), i had this error, every time i did a GET or POST. The serializer and deserializer, failed to convert Decimal types into any JSON identifiable format.

    I did a "pip install simplejson", then Just by adding

    import simplejson as json
    

    the serializer and deserializer starts to purr again. I did nothing else... DEciamls are displayed as '234.00' float format.

    0 讨论(0)
  • 2020-11-22 09:23

    Based on stdOrgnlDave answer I have defined this wrapper that it can be called with optional kinds so the encoder will work only for certain kinds inside your projects. I believe the work should be done inside your code and not to use this "default" encoder since "it is better explicit than implicit", but I understand using this will save some of your time. :-)

    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 讨论(0)
  • 2020-11-22 09:25

    From the JSON Standard Document, as linked in json.org:

    JSON is agnostic about the semantics of numbers. In any programming language, there can be a variety of number types of various capacities and complements, fixed or floating, binary or decimal. That can make interchange between different programming languages difficult. JSON instead offers only the representation of numbers that humans use: a sequence of digits. All programming languages know how to make sense of digit sequences even if they disagree on internal representations. That is enough to allow interchange.

    So it's actually accurate to represent Decimals as numbers (rather than strings) in JSON. Bellow lies a possible solution to the problem.

    Define a custom JSON encoder:

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

    Then use it when serializing your data:

    json.dumps(data, cls=CustomJsonEncoder)
    

    As noted from comments on the other answers, older versions of python might mess up the representation when converting to float, but that's not the case anymore.

    To get the decimal back in Python:

    Decimal(str(value))
    

    This solution is hinted in Python 3.0 documentation on decimals:

    To create a Decimal from a float, first convert it to a string.

    0 讨论(0)
提交回复
热议问题