Search code examples
pythonjsonjsonserializer

Support default serialisation for a custom class in a nested type


I defined a class A and I am using objects of A inside other classes, containers and nested types.

Example

a = A()
b = [a,3,'hello'
c = {'hey': b, 'huy': 3}
d = [a,b,c]

I am interested in the json representation of d, so of course I have to specify how to behave when encountering an instance of A.

For an object of class 'A' I want to get this (note that I am skipping att2, this shows that I don't what to use something like a.__dict__):

{
    class_name = 'A',
    att1 = '<value of att1>',
    att3 = '<value of att3>'
}

att1 can be anything: an A instance, a default type, etc.

I would like to be able to call json.dumps(d) and having the desired output, so what I trying to do is to tell the class A what to output when asked by the json serializer.

I looked around and I have only found complex solutions in which a lambda or a class is passed to json.dumps() and I can't figure out why not just overriding a method called by the json serialiser, like when overriding __str__() or __repr__().

Maybe this is not possible? In such a case what could be a simple solution in my case? Note that in my real problem I have not only the class A but various custom classes nested together, so overriding the default serialiser would be the natural and easier solution.

One of my attempts

I am trying to follow the suggestion of line 160 of encoder.py

So subclassed A from json.JSONEncoder and I declared the method

def default(self,obj):
    return 'valid custom json representation of A'

but this is not working and the exception of line 179 of the above link is raised. What is wrong with my code?


Solution

  • The default JSON encoder (that you linked to) only knows how to handle "easy" things like dicts, lists, strings, and numbers. It makes no attempt to serialize classes so there is no "special" method you can implement on a class to make it JSON serializable. Basically, if you want to use json.dumps, you're going to have to pass something extra in order to make it work with your custom classes.

    When the encoder doesn't know what to do with the object, it calls the default method. Based on your question, I believe you discovered that already but you're a bit confused about where the default method should go. Here's an example that does what you're looking for:

    import json
    
    def default(o):
        if hasattr(o, 'to_json'):
            return o.to_json()
        raise TypeError(f'Object of type {o.__class__.__name__} is not JSON serializable')
    
    class A(object):
        def __init__(self):
            self.data = 'stuff'
            self.other_data = 'other stuff'
    
        def to_json(self):
            return {'data': self.data}
    
    a = A()
    b = [a, 3, 'hello']
    c = {'hey': b, 'huy': 3}
    d = [a, b, c]
    
    print(json.dumps(d, default=default))
    

    Which prints out:

    [{"data": "stuff"}, [{"data": "stuff"}, 3, "hello"], {"hey": [{"data": "stuff"}, 3, "hello"], "huy": 3}]
    

    Note that every custom class just needs to implement the to_json method now and everything will serialize correctly (assuming you pass the custom default function in when calling json.dumps).