Python Custom Class to JSON: Fix Not JSON Serializable

Fix TypeError Object is not JSON serializable in Python with a default function, a JSONEncoder subclass, dataclasses.asdict() or __dict__, with dates.

Python

Python’s json module only knows how to write dictionaries, lists, tuples, strings, numbers, booleans and None, so a custom class needs a function that turns each object into one of those types before json.dumps() can write it. Without that function, the call fails with TypeError: Object of type Booking is not JSON serializable.

import json
from dataclasses import dataclass
from datetime import date

@dataclass
class Booking:
    guest: str
    nights: int
    check_in: date

booking = Booking("Lokesh", 3, date(2026, 11, 2))
bad = json.dumps(booking)    # TypeError: Object of type Booking is not JSON serializable

We fix the error four ways in this article, with a default function, a JSONEncoder subclass, dataclasses.asdict() and the quick __dict__ shortcut, and we compare them at the end. All of it was tested on Python 3.14.

1. A default Function

The default argument of json.dumps() and json.dump() takes a function. The json module calls it for every object it cannot write, and the function returns a serializable replacement or raises TypeError.

import json

def to_json(obj):
    if isinstance(obj, Booking):
        return {"guest": obj.guest, "nights": obj.nights, "check_in": obj.check_in}
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"Cannot serialize {type(obj).__name__}")

text = json.dumps(booking, default=to_json)
# {"guest": "Lokesh", "nights": 3, "check_in": "2026-11-02"}

The date inside the dictionary goes through the same function, which is why one function handles both types. The final raise TypeError matters, because returning None for unknown types would write null and hide the bug.

2. A JSONEncoder Subclass

When the same rules apply across a project, we put them in a subclass of json.JSONEncoder and pass it with cls. Its default() method works like the function above and hands unknown types to the parent class, which raises the standard error.

class BookingEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Booking):
            return {"guest": obj.guest, "nights": obj.nights, "check_in": obj.check_in}
        if isinstance(obj, date):
            return obj.isoformat()
        return super().default(obj)

text2 = json.dumps(booking, cls=BookingEncoder, indent=2)
{
  "guest": "Lokesh",
  "nights": 3,
  "check_in": "2026-11-02"
}

3. Dataclasses with asdict()

For a dataclass, dataclasses.asdict() builds the dictionary for us, nested dataclasses included. Values such as date still need a default, and default=str is enough for dates and Decimal.

from dataclasses import asdict

text3 = json.dumps(asdict(booking), default=str)
# {"guest": "Lokesh", "nights": 3, "check_in": "2026-11-02"}

The shortcut default=str turns any unknown object into its string form, so it can write unreadable text such as <__main__.Guest object at 0x…> for classes we forgot. We use it only when the data contains types with a useful str().

4. The __dict__ Shortcut and Its Limits

Many answers online pass default=lambda o: o.__dict__. It works for plain classes, but it writes every attribute (private ones too), fails for classes with __slots__ and still cannot write a date.

class Guest:
    def __init__(self, name, vip):
        self.name = name
        self.vip = vip

text4 = json.dumps(Guest("Lokesh", True), default=lambda o: o.__dict__)
# {"name": "Lokesh", "vip": true}
ApproachBest forWatch out for
default=functionOne-off conversions, several typesRaise TypeError for unknown types
JSONEncoder subclassRules shared across a projectCall super().default() at the end
asdict() plus default=strDataclassesA set field becomes a string such as “{1, 2}”, not an array
default=lambda o: o.__dict__Quick scriptsPrivate fields, __slots__, dates

To turn the JSON back into Booking objects, see custom class deserialization in Python.

5. Nested Objects, Enums and Sets

Real data nests objects inside lists and dictionaries, and it often contains an Enum or a set. A single default function handles all of them, because the json module calls it again for every value it cannot write.

from enum import Enum

class Room(Enum):
    SINGLE = "single"
    DOUBLE = "double"

def to_json2(obj):
    if isinstance(obj, Booking):
        return asdict(obj)
    if isinstance(obj, date):
        return obj.isoformat()
    if isinstance(obj, Enum):
        return obj.value
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Cannot serialize {type(obj).__name__}")

data = {"room": Room.DOUBLE, "tags": {"sea", "quiet"}, "bookings": [booking]}
text5 = json.dumps(data, default=to_json2)
# {"room": "double", "tags": ["quiet", "sea"], "bookings": [{"guest": "Lokesh", "nights": 3, "check_in": "2026-11-02"}]}

Sorting the set makes the output stable between runs, which helps in tests and diffs. Since Python 3.14, the TypeError for an unsupported value also carries notes that show where the value sits in the nested data.

6. Serializing Other Common Types

These types cause most “not JSON serializable” errors in practice.

6.1. How do we serialize a datetime?

We convert it with isoformat(), which gives a string such as “2026-11-02T09:30:00”. On the way back, datetime.fromisoformat() parses the same string.

6.2. How do we serialize a Decimal without losing precision?

We return str(obj) from the default function, so Decimal(“9.99”) becomes “9.99”. Converting to float can change the value slightly.

6.3. What about Pydantic models?

A Pydantic model has its own model_dump_json() method, which handles dates, enums and nested models. A model placed inside other data that goes to json.dumps() still needs a default function, for example one that returns obj.model_dump(mode=”json”).

7. Conclusion

A custom class becomes JSON once something converts it to a dictionary. A default function is the smallest fix, a JSONEncoder subclass keeps shared rules in one place, and asdict() does most of the work for dataclasses. For dates, ISO 8601 strings from isoformat() are the format other systems read most reliably.

8. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.