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 serializableBy 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:
- Fallback to Base Class: Always end the overridden
default()method withreturn super().default(o). This ensures that unhandled object types correctly raise aTypeError. - Recursive Resolution: The encoder only needs to
transform the custom object one level down into standard primitives or
containers. If the returned dictionary contains another custom object,
the
jsonmodule will recursively invokedefault()on that object as well. - Dataclasses Integration: If using Python's
dataclasses, you can combinedataclasses.asdict()inside your custom encoder to convert structured objects to dictionaries automatically without manually mapping every attribute.