Home  >  Article  >  Backend Development  >  How can I make custom objects JSON serializable without subclassing `json.JSONEncoder`?

How can I make custom objects JSON serializable without subclassing `json.JSONEncoder`?

Barbara Streisand
Barbara StreisandOriginal
2024-10-30 20:25:30355browse

How can I make custom objects JSON serializable without subclassing `json.JSONEncoder`?

Making objects JSON serializable with the regular encoder

The default way to serialize custom non-serializable objects to JSON is to subclass json.JSONEncoder and pass a custom encoder to json.dumps(). This typically looks like the following:

<code class="python">class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Foo):
            return obj.to_json()

        return json.JSONEncoder.default(self, obj)

print(json.dumps(obj, cls=CustomEncoder))</code>

However, what if you want to make an object serializable with the default encoder? After reviewing the json module's source code, it appears that extending the encoder directly will not meet this requirement.

Instead, you can use a technique called "monkey-patching" within your package's __init__.py initialization script. This affects all subsequent JSON module serializations since modules are generally loaded only once, and the result is cached in sys.modules.

The patch would modify the default JSON encoder's default method to check for a unique "to_json" method and utilize it to encode the object if found.

Here's an example implemented as a standalone module for simplicity:

<code class="python"># Module: make_json_serializable.py

from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder.default  # Save unmodified default.
JSONEncoder.default = _default  # Replace it.</code>

Using this patch is simple: just import the module to apply the monkey-patch.

<code class="python"># Sample client script

import json
import make_json_serializable  # apply monkey-patch

class Foo(object):
    def __init__(self, name):
        self.name = name

    def to_json(self):  # New special method.
        """Convert to JSON format string representation."""
        return '{"name": "%s"}' % self.name

foo = Foo('sazpaz')
print(json.dumps(foo))  # -> '{"name": "sazpaz"}'</code>

To retain object type information, the to_json method can include it in the returned string:

<code class="python">def to_json(self):
    """Convert to JSON format string representation."""
    return '{"type": "%s", "name": "%s"}' % (self.__class__.__name__, self.name)</code>

This produces JSON that includes the class name:

{"type": "Foo", "name": "sazpaz"}

Magick Lies Here

An even more powerful approach is to have the replacement default method serialize most Python objects automatically, including user-defined class instances, without requiring a unique method.

After researching several alternatives, the following approach based on pickle appears closest to this ideal:

<code class="python"># Module: make_json_serializable2.py

from json import JSONEncoder
import pickle

def _default(self, obj):
    return {"_python_object": pickle.dumps(obj)}

JSONEncoder.default = _default  # Replace with the above.</code>

While not everything can be pickled (e.g., extension types), pickle provides methods to handle them via a protocol using unique methods. However, this approach covers more cases.

Deserializing

Using the pickle protocol simplifies reconstructing the original Python object by providing a custom object_hook function argument to json.loads() when encountering a "_python_object" key in the dictionary.

<code class="python">def as_python_object(dct):
    try:
        return pickle.loads(str(dct['_python_object']))
    except KeyError:
        return dct

pyobj = json.loads(json_str, object_hook=as_python_object)</code>

This can be simplified to a wrapper function:

<code class="python">json_pkloads = functools.partial(json.loads, object_hook=as_python_object)

pyobj = json_pkloads(json_str)</code>

This code does not work in Python 3 because json.dumps() returns a bytes object that JSONEncoder cannot handle. However, the approach remains valid with the following modification:

<code class="python">def _default(self, obj):
    return {"_python_object": pickle.dumps(obj).decode('latin1')}

def as_python_object(dct):
    try:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    except KeyError:
        return dct</code>

The above is the detailed content of How can I make custom objects JSON serializable without subclassing `json.JSONEncoder`?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn