Lets say we have a custom type:

class Foo:
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return f'Foo("{self.value}")'

How to use/extend json.JSONEncoder and json.JSONDecoder so that instances of Foo would be serialized and deserialized? Expectation:

>>> foo = Foo('bar')
>>> foo
... Foo("bar")
>>> FooJSONEncoder().encode([foo])
... '[Foo("bar")]'
>>> FooJSONDecoder().decode('[Foo("bar")]')
... [Foo("bar")]

I have tried overriding json.JSONEncoder.default(o):

class FooJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Foo):
            return f'Foo("{o.value}")'
        return super().default(o)

But custom type gets converted to string and it gets deserialized as a string.

>>> FooJSONEncoder().encode([foo])
... '["Foo(\\"bar\\")"]'
>>> json.JSONDecoder().decode('["Foo(\\"bar\\")"]')
... ['Foo("bar")']

1 Answers

0
niekas On Best Solutions

A custom type should be serialized to a dictionary (not a string and not a Foo type as provided in the expectations):

class FooJSONEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Foo):
            return {'__type__': o.__class__.__name__, 'value': o.value}
        return super().default(o)

Now foo instance serializes to:

>>> FooJSONEncoder().encode(foo)
... '{"__type__": "Foo", "value": "bar"}'

In order to decerialize it, a custom json.JSONDecoder.object_hook has to be implemented:

def foo_hook(dct):
    if dct.get('__type__') == 'Foo':
        return Foo(value=dct.get('value'))
    return dct

And provided for the JSONDecoder, e.g.:

>>> serialized_foo = FooJSONEncoder().encode(foo)
>>> json.JSONDecoder(object_hook=foo_hook).decode(serialized_foo)
... Foo("bar")
>>> json.loads(serialized_foo, object_hook=foo_hook)
... Foo("bar")