I'm trying to use jsonpickle in python 3.7 to serialize an object tree to json. However, all Decimal
s are serialized as null
. I'm using simplejson as a backend, so that should be able to serialize Decimals.
How do I serialize a (complex) object tree to json, including Decimals?
Example code (requires simplejson and jsonpickle to be installed):
Expected serialized json should be {"amount": 1.0}
and I do not want to use float
, because of rounding errors.
import jsonpickle
from decimal import Decimal
jsonpickle.set_preferred_backend('simplejson')
jsonpickle.set_encoder_options('simplejson', use_decimal=True)
class MyClass():
def __init__(self, amount):
self.amount = amount
def to_json(self):
return jsonpickle.dumps(self, unpicklable=False)
if __name__ == '__main__':
obj = MyClass(Decimal('1.0'))
print(obj.to_json()) # prints '{"amount": null}'
PS I don't care about using jsonpickle. So alternatives to jsonpickle to serialize a complex object tree to json (including Decimal fields) are welcome as well.
Updated answer: jsonpickle's master branch now has a use_decimal mode that allows you to achieve this result without any custom handlers.
import decimal
import unittest
import jsonpickle
class Example(object):
"""Example class holding a Decimal"""
def __init__(self, amount):
self.amount = decimal.Decimal(amount)
class UseDecimalTestCase(unittest.TestCase):
"""Demonstrate the new use_decimal mode"""
def test_use_decimal(self):
obj = Example(0.5)
# Configure simplejson to use decimals.
jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True)
jsonpickle.set_preferred_backend('simplejson')
as_json = jsonpickle.dumps(obj, unpicklable=False, use_decimal=True)
print(as_json)
# {"amount": 0.5}
# Configure simplejson to get back Decimal when restoring from json.
jsonpickle.set_decoder_options('simplejson', use_decimal=True)
obj_clone = jsonpickle.loads(as_json)
# NOTE: we get back a dict, not an Example instance.
self.assertTrue(isinstance(obj_clone, dict))
# But, the Decimal *is* preserved
self.assertTrue(isinstance(obj_clone['amount'], decimal.Decimal))
self.assertEqual(obj.amount, obj_clone['amount'])
# Side-effect of simplejson decimal mode:
# floats become Decimal when round-tripping
obj.amount = 0.5 # float
as_json = jsonpickle.dumps(obj, unpicklable=False)
obj_clone = jsonpickle.loads(as_json)
self.assertTrue(isinstance(obj_clone['amount'], decimal.Decimal))
if __name__ == '__main__':
unittest.main()
Related issue:
https://github.com/jsonpickle/jsonpickle/issues/244
For older jsonpickle versions:
This can be done with a custom pass-through handler that'll allow simplejson to do the encoding. You have to configure both the encoder and decoder options so that you get back decimals. If you don't care about round-tripping then the use case is simpler.
import decimal
import unittest
import jsonpickle
from jsonpickle.handlers import BaseHandler
class SimpleDecimalHandler(BaseHandler):
"""Simple pass-through handler so that simplejson can do the encoding"""
def flatten(self, obj, data):
return obj
def restore(self, obj):
return obj
class Example(object):
"""Example class holding a Decimal"""
def __init__(self, amount):
self.amount = decimal.Decimal(amount)
class DecimalTestCase(unittest.TestCase):
"""Test Decimal json serialization"""
def test_custom_handler(self):
obj = Example(0.5)
# Enable the simplejson Decimal handler -- slightly simpler than jsonpickle's
# default handler which does the right thing already.
# If you don't care about the json representation then you don't
# need to do anything -- jsonpickle preserves decimal by default
# when using its default dumps() options.
#
# We use this decimal handler so that simplejson does the encoding
# rather than jsonpickle. Thus, we have to configure simplejson too,
# which is not needed otherwise when using jsonpickle's defaults.
jsonpickle.set_encoder_options('simplejson', use_decimal=True, sort_keys=True)
jsonpickle.set_decoder_options('simplejson', use_decimal=True)
jsonpickle.set_preferred_backend('simplejson')
SimpleDecimalHandler.handles(decimal.Decimal)
as_json = jsonpickle.dumps(obj)
print(as_json)
# {"amount": 0.5, "py/object": "__main__.Example"}
# NOTE: this comes back as an Example instance
clone = jsonpickle.loads(as_json)
self.assertTrue(isinstance(clone, Example))
self.assertTrue(isinstance(clone.amount, decimal.Decimal))
self.assertEqual(obj.amount, clone.amount)
# We can simplify the JSON representation a little further
# by using unpickleable=False, but we lose the Example class.
as_json = jsonpickle.dumps(obj, unpicklable=False)
# Upside: this prints {"amount": 0.5}
# Downside: this object cannot be reconstructed back into an
# instance of the Example class.
print(as_json)
# NOTE: we get back a dict, not an Example instance.
obj_clone = jsonpickle.loads(as_json)
self.assertTrue(isinstance(obj_clone, dict))
# But, the Decimal *is* preserved
self.assertTrue(isinstance(obj_clone['amount'], decimal.Decimal))
self.assertEqual(obj.amount, obj_clone['amount'])
if __name__ == '__main__':
unittest.main()