calvinkrishy calvinkrishy - 1 year ago 450
JSON Question

Serializing a Python namedtuple to json

What is the recommended way of serializing a

to json with the field names retained?

Serializing a
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
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?

Answer Source

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]

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)
            gen = JSONEncoder._iterencode(self, obj, markers)
        for chunk in gen:
            yield chunk

class foobar(namedtuple('f', 'foo, bar')):

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"}}