pythonjsonnamedtuple

Serializing a Python namedtuple to json


What is the recommended way of serializing a namedtuple to json with the field names retained?

Serializing a namedtuple to json results in only the values being serialized and the field names being lost in translation. I would like the fields also to be retained when json-ized and hence did the following:

class foobar(namedtuple('f', 'foo, bar')):
    __slots__ = ()
    def __iter__(self):
        yield self._asdict()

The above serializes to json as I expect and behaves as namedtuple in other places I use (attribute access etc.,) except with a non-tuple like results while iterating it (which fine for my use case).

What is the "correct way" of converting to json with the field names retained?


Solution

  • This is pretty tricky, since namedtuple() is a factory which returns a new type derived from tuple. One approach would be to have your class also inherit from UserDict.DictMixin, but tuple.__getitem__ is already defined and expects an integer denoting the position of the element, not the name of its attribute:

    >>> f = foobar('a', 1)
    >>> f[0]
    'a'
    

    At its heart the namedtuple is an odd fit for JSON, since it is really a custom-built type whose key names are fixed as part of the type definition, unlike a dictionary where key names are stored inside the instance. This prevents you from "round-tripping" a namedtuple, e.g. you cannot decode a dictionary back into a namedtuple without some other a piece of information, like an app-specific type marker in the dict {'a': 1, '#_type': 'foobar'}, which is a bit hacky.

    This is not ideal, but if you only need to encode namedtuples into dictionaries, another approach is to extend or modify your JSON encoder to special-case these types. Here is an example of subclassing the Python json.JSONEncoder. This tackles the problem of ensuring that nested namedtuples are properly converted to dictionaries:

    from collections import namedtuple
    from json import JSONEncoder
    
    class MyEncoder(JSONEncoder):
    
        def _iterencode(self, obj, markers=None):
            if isinstance(obj, tuple) and hasattr(obj, '_asdict'):
                gen = self._iterencode_dict(obj._asdict(), markers)
            else:
                gen = JSONEncoder._iterencode(self, obj, markers)
            for chunk in gen:
                yield chunk
    
    class foobar(namedtuple('f', 'foo, bar')):
        pass
    
    enc = MyEncoder()
    for obj in (foobar('a', 1), ('a', 1), {'outer': foobar('x', 'y')}):
        print enc.encode(obj)
    
    {"foo": "a", "bar": 1}
    ["a", 1]
    {"outer": {"foo": "x", "bar": "y"}}