tags:

views:

290

answers:

5

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 precision on the client side, so a float is fine.

Is there a good way to serialize this? JSONDecoder doesn't accept Decimal objects, and converting to a float beforehand yields {'x': 3.8999999999999999} which is wrong, and will be a big waste of bandwidth.

+1  A: 

3.9 can not be exactly represented in IEEE floats, it will always come as 3.8999999999999999, e.g. try print repr(3.9), read more at
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 dumping of decimal objects 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)
Anurag Uniyal
I know it won't be 3.9 internally once it is parsed on the client, but 3.9 is a valid JSON float.ie, `json.loads("3.9")` will work, and I would like it to be this
Knio
@Anurag You meant repr(obj) instead of repr(o) in your example.
orokusaki
@orokusaki, yes thanks.
Anurag Uniyal
A: 

this can be done by adding

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

in \Lib\json\encoder.py:JSONEncoder._iterencode, but I was hoping for a better solution

Knio
+4  A: 

How about subclassing 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)

Then use it like so:

json.dumps({'x': decimal.Decimal('5.5')}, cls=DecimalEncoder)
Michał Marczyk
This makes sense. Thanks
Knio
Ouch, I just noticed that it won't actually work like this. Will edit accordingly. (The idea stays the same, though.)
Michał Marczyk
The problem was that `DecimalEncoder()._iterencode(decimal.Decimal('3.9')).next()` returned the correct `'3.9'`, but `DecimalEncoder()._iterencode(3.9).next()` returned a generator object which would only return `'3.899...'` when you piled on another `.next()`. Generator funny business. Oh well... Should work now.
Michał Marczyk
Wow, this got me a downvote w/o any sort of comment? If there's something the matter with this code, I'd love to know, as I'm liable to use something similar -- not to mention the OP might like to find out.
Michał Marczyk
@Michael There is parse_float=decimal.Decimal for deserialization. It almost seems like a bug to not have the same functionality in serialization. I mean Decimal is a native Python format after all. P.S. I voted you back up to make up for the clown.
orokusaki
@orokusaki: Thanks! As for Decimal and JSON serialisation, I know where you're at, though I suppose the rationale behind the current state of things was that the programmer is to be warned about the loss of precision in JSON serialisation... Like the need to convert a float to string prior to turning it into a Decimal, even though a float-to-Decimal maximal-precision-preserving decimal.Decimal constructor would be a nice facility. Oh well.
Michał Marczyk
@Michal Yeah. Only after posting that comment yesterday did I learn that JavaScript handles decimal formatted numbers as normal floats. I don't know why I was so optimistic about JavaScript having some sort of magic like decimal.Decimal() but now it's clear (and after this last comment of yours it's also clear why this parse_float stuff was left out when serializing. I'll end up using your class. It seems my clients can parseFloat() on my output if they want to do pseudo-math for their pages, and that should work fine.
orokusaki
@orokusaki: Glad to know this is useful for you! :-)
Michał Marczyk
Simplejson has native support for Decimals - see my answer: http://stackoverflow.com/questions/1960516/python-json-serialize-a-decimal-object/3148376#3148376
Lukas Cenovsky
+1  A: 

Simplejson 2.1 and higher has native support for Decimal type:

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

Hopefully, this feature will be included in standard library.

Lukas Cenovsky
easiest fix! thanks for the answer!
cce
Hmm, for me this converts Decimal objects to floats, which is not acceptable. Loss of precision when working with currency, for instance.
Matthew Schinckel
+2  A: 

I would like to let everyone know that I tried Michał Marczyk's answer on my web server that was running Python 2.6.5 and it worked fine. However, I upgraded to Python 2.7 and it stopped working. I tried to think of some sort of way to encode Decimal objects and this is what I came up with:

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

This should hopefully help anyone who is having problems with Python 2.7. I tested it and it seems to work fine. If anyone notices any bugs in my solution or comes up with a better way, please let me know.

mikez302