I am trying to subclass json.JSONEncoder so that named tuples (defined using the new Python 3.6+ syntax, but it probably still refers to the output of collections.namedtuple ) are serialized into JSON objects where the fields of the tuples correspond to the objects.
For instance:
from typing import NamedTuple class MyModel(NamedTuple): foo:int bar:str = "Hello, World!" a = MyModel(123) # Expected JSON: {"foo": 123, "bar": "Hello, World!"} b = MyModel(456, "xyzzy") # Expected JSON: {"foo": 456, "bar": "xyzzy"}
I understand that I am subclassing json.JSONEncoder and overriding its default method to ensure serialization for new types. The rest of the class will do the right thing with respect to recursion, etc. So I came to the following:
class MyJSONEncoder(json.JSONEncoder): def default(self, o): to_encode = None if isinstance(o, tuple) and hasattr(o, "_asdict"):
This works when it tries to serialize (i.e., as the cls parameter on json.dumps ) the datetime value - at least partially prove my hypothesis - but the check for named tuples never hits and by default it serializes it as a tuple (i.e. i.e. into a JSON array). Strange, I suggested that I should call the superclass default method on my converted object, but then an exception occurs when it tries to serialize a datetime : "TypeError: Object of type" str "is not serializable JSON", which frankly makes no sense!
I get the same behavior if I make the type checking of the named type more specific (e.g. isinstance(o, MyModel) ). However, I found that I can almost get the behavior I'm looking for if I also override the encode method by moving the named check there:
class AlmostWorkingJSONEncoder(json.JSONEncoder): def default(self, o): to_encode = None if isinstance(o, datetime):
This works, but not recursively: it successfully serializes top-level tuples into JSON objects according to my requirement, but any named tuples that exist in this named tuple will be serialized by default (JSON array). This is also the behavior if I put the specified tuple type check into the default and encode methods.
The documentation implies that only the default method should be changed in subclasses. I believe, for example, that redefining encode in AlmostWorkingJSONEncoder will break it when it performs interleaved encoding. However, no number of hackers have yet yielded what I want (or expect what will happen, given the scant documentation).
Where is my misunderstanding?
EDIT Reading the code for json.JSONEncoder explains why the default method causes a type error when you pass it a line: it does not clear (at least from me) from the documentation, but the default method is designed to convert the values โโof some unsupported type into a serializable type, which then returns; if an unsupported type is not converted to any of your overridden method, then you must call super().default(o) at the end to cause a type error. So something like this:
class SubJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, Foo): return SerialisableFoo(o) if isinstance(o, Bar): return SerialisableBar(o)
I believe that the problem I am facing is that the default method is only called by the encoder when it cannot match any supported types. The named tuple is still a tuple that is supported, so it matches the first before delegating my overridden default method. In Python 2.7, the functions that performed this mapping are part of the JSONEncoder object, but in Python 3 they seem to be moved outside the module namespace (and therefore not available to the user area). Thus, I believe that the JSONEncoder subclass JSONEncoder not allow serialization of the named tuples in a general way, without doing a lot of rewriting and tight binding to your own implementation JSONEncoder
EDIT 2 I presented this as a bug .