How to change JSON encoding behaviour for serializable Python object?
json, python
Solution
It seems that to achieve the behavior you want, with the given restrictions, you'll have to delve into the `JSONEncoder` class a little. Below I've written out a custom `JSONEncoder` that overrides the `iterencode` method to pass a custom `isinstance` method to `_make_iterencode`. It isn't the cleanest thing in the world, but seems to be the best given the options and it keeps customization to a minimum.
# customencoder.py
from json.encoder import (_make_iterencode, JSONEncoder,
encode_basestring_ascii, FLOAT_REPR, INFINITY,
c_make_encoder, encode_basestring)
class CustomObjectEncoder(JSONEncoder):
def iterencode(self, o, _one_shot=False):
"""
Most of the original method has been left untouched.
_one_shot is forced to False to prevent c_make_encoder from
being used. c_make_encoder is a funcion defined in C, so it's easier
to avoid using it than overriding/redefining it.
The keyword argument isinstance for _make_iterencode has been set
to self.isinstance. This allows for a custom isinstance function
to be defined, which can be used to defer the serialization of custom
objects to the default method.
"""
# Force the use of _make_iterencode instead of c_make_encoder
_one_shot = False
if self.check_circular:
markers = {}
else:
markers = None
if self.ensure_ascii:
_encoder = encode_basestring_ascii
else:
_encoder = encode_basestring
if self.encoding != 'utf-8':
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
if isinstance(o, str):
o = o.decode(_encoding)
return _orig_encoder(o)
def floatstr(o, allow_nan=self.allow_nan,
_repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY):
if o != o:
text = 'NaN'
elif o == _inf:
text = 'Infinity'
elif o == _neginf:
text = '-Infinity'
else:
return _repr(o)
if not allow_nan:
raise ValueError(
"Out of range float values are not JSON compliant: " +
repr(o))
return text
# Instead of forcing _one_shot to False, you can also just
# remove the first part of this conditional statement and only
# call _make_iterencode
if (_one_shot and c_make_encoder is not None
and self.indent is None and not self.sort_keys):
_iterencode = c_make_encoder(
markers, self.default, _encoder, self.indent,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, self.allow_nan)
else:
_iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, _one_shot, isinstance=self.isinstance)
return _iterencode(o, 0)
You can now subclass the `CustomObjectEncoder` so it correctly serializes your custom objects. The `CustomObjectEncoder` can also do cool stuff like handle nested objects.
# test.py
import json
import datetime
from customencoder import CustomObjectEncoder
class MyEncoder(CustomObjectEncoder):
def isinstance(self, obj, cls):
if isinstance(obj, (mList, mDict)):
return False
return isinstance(obj, cls)
def default(self, obj):
"""
Defines custom serialization.
To avoid circular references, any object that will always fail
self.isinstance must be converted to something that is
deserializable here.
"""
if isinstance(obj, datetime.datetime):
return obj.isoformat()
elif isinstance(obj, mDict):
return {"orig": dict(obj), "attrs": vars(obj)}
elif isinstance(obj, mList):
return {"orig": list(obj), "attrs": vars(obj)}
else:
return None
class mList(list):
pass
class mDict(dict):
pass
def main():
zelda = mList(['zelda'])
zelda.src = "oldschool"
games = mList(['mario', 'contra', 'tetris', zelda])
games.src = 'console'
scores = mDict({'dp': 10, 'pk': 45})
scores.processed = "unprocessed"
test_json = {'games': games, 'scores': scores,
'date': datetime.datetime.now()}
print(json.dumps(test_json, cls=MyEncoder))
if __name__ == '__main__':
main()
Problem
It is easy to change the format of an object which is not JSON serializable, e.g. `datetime.datetime`. My requirement, for debugging purposes, is to alter the way some custom objects extended from base ones like `dict` and `list` , get serialized in JSON format. Code: ``` import datetime import json def json_debug_handler(obj): print("object received:") print type(obj) print("\n\n") if isinstance(obj, datetime.datetime): return obj.isoformat() elif isinstance(obj,mDict): return {'orig':obj , 'attrs': vars(obj)} elif isinstance(obj,mList): return {'orig':obj, 'attrs': vars(obj)} else: return None class mDict(dict): pass class mList(list): pass def test_debug_json(): games = mList(['mario','contra','tetris']) games.src = 'console' scores = mDict({'dp':10,'pk':45}) scores.processed = "unprocessed" test_json = { 'games' : games , 'scores' : scores , 'date': datetime.datetime.now() } print(json.dumps(test_json,default=json_debug_handler)) if __name__ == '__main__': test_debug_json() ``` Demo Output: ``` {"date": "2013-05-07T01:03:13.098727", "games": ["mario", "contra", "tetris"], "scores": {"pk": 45, "dp": 10}} ``` Desired output: ``` {"date": "2013-05-07T01:03:13.098727", "games": { "orig": ["mario", "contra", "tetris"] ,"attrs" : { "src":"console"}} , "scores": { "orig": {"pk": 45, "dp": 10},"attrs": "processed":"unprocessed }} ``` Does the `default` handler not work for serializable objects? If not, how can I override this, without adding `toJSON` methods to the extended classes? Also, there is this version of JSON encoder which does not work: ``` class JsonDebugEncoder(json.JSONEncoder): def default(self,obj): if isinstance(obj, datetime.datetime): return obj.isoformat() elif isinstance(obj,mDict): return {'orig':obj , 'attrs': vars(obj)} elif isinstance(obj,mList): return {'orig':obj, 'attrs': vars(obj)} else: return json.JSONEncoder.default(self, obj) ``` If there is a hack with `pickle,__getstate__,__setstate__,`and then using `json.dumps` over `pickle.loads` object, I am open to that as well. I tried, but that did not work.
Related problems
- Can you patch *just* a nested function with closure, or must the whole outer function be repeated?
- Python, override__getstate__() and __setstate__()
- class method __instancecheck__ does not work
- Are you allowed to modify func_defaults (__defaults__ in Python 3.x) in Python?
- Overriding nested JSON encoding of inherited default supported objects like dict, list