Custom Object Serialization with Python JSON Encoders

Python's built-in json module provides native support for serializing primitive data types like dictionaries, lists, strings, and integers, but it cannot serialize custom user-defined objects out of the box. To convert custom Python class instances into JSON, the module provides hooks to define custom serialization logic. This article explains how to implement custom encoders using the default parameter and by subclassing json.JSONEncoder to convert custom objects into serializable data structures.

The Default Serialization Limitation

When you attempt to pass a custom Python class instance directly into json.dumps(), the interpreter raises a TypeError:

class User:
    def __init__(self, name: str, user_id: int):
        self.name = name
        self.user_id = user_id

user = User("Alice", 101)
json.dumps(user)  # Raises TypeError: Object of type User is not JSON serializable

By design, the json module only knows how to represent types that map directly to standard JSON equivalents: dict, list, tuple, str, int, float, bool, and None.

Method 1: Using the default Parameter

The most straightforward way to serialize a custom object is providing a callable to the default parameter in json.dumps(). The json module invokes this function whenever it encounters an object it cannot serialize natively.

import json

def user_serializer(obj):
    if isinstance(obj, User):
        return {"name": obj.name, "user_id": obj.user_id}
    raise TypeError(f"Type {type(obj)} not serializable")

json_output = json.dumps(user, default=user_serializer)
print(json_output)  # Output: {"name": "Alice", "user_id": 101}

The function must return a serializable object (such as a dictionary, list, or primitive) or raise a TypeError for objects it does not support.

Method 2: Subclassing json.JSONEncoder

For complex projects or reusable encoding logic, the standard approach is subclassing json.JSONEncoder and overriding its default() method.

import json

class CustomEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, User):
            return {
                "name": o.name,
                "user_id": o.user_id,
                "__class__": o.__class__.__name__
            }
        # Let the base class default method raise the TypeError
        return super().default(o)

To apply this custom encoder, pass the class reference to the cls parameter in json.dumps():

json_output = json.dumps(user, cls=CustomEncoder, indent=2)
print(json_output)

Output:

{
  "name": "Alice",
  "user_id": 101,
  "__class__": "User"
}

Important Execution Details: