How to change json encoding behaviour for serializable python object?

前端 未结 13 1240
无人及你
无人及你 2020-12-02 09:47

It is easy to change the format of an object which is not JSON serializable eg datetime.datetime.

My requirement, for debugging purposes, is to alter the way some cu

13条回答
  •  眼角桃花
    2020-12-02 10:17

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

提交回复
热议问题