Python
JSON
dataclasses
encoder
serialization

Make the Python json encoder support Python's new dataclasses

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python dataclasses are convenient for typed domain models, but json.dumps cannot serialize them automatically. You need a conversion rule that turns dataclass instances into JSON-compatible structures. A reusable custom encoder is usually the cleanest approach, especially when your payload also includes values such as datetime, UUID, or Decimal.

Why Default Encoding Fails

The built-in JSON encoder only handles primitive JSON-compatible types and standard containers. A dataclass instance is still a custom object.

python
1import json
2from dataclasses import dataclass
3
4@dataclass
5class User:
6    id: int
7    name: str
8
9u = User(1, "Mina")
10
11try:
12    print(json.dumps(u))
13except TypeError as err:
14    print("error:", err)

You will see a TypeError because serializer does not know how to convert User.

Basic Dataclass-Aware Encoder

Use is_dataclass and asdict in a custom JSONEncoder subclass.

python
1import json
2from dataclasses import dataclass, asdict, is_dataclass
3
4class DataclassEncoder(json.JSONEncoder):
5    def default(self, obj):
6        if is_dataclass(obj):
7            return asdict(obj)
8        return super().default(obj)
9
10@dataclass
11class Address:
12    city: str
13    country: str
14
15@dataclass
16class User:
17    id: int
18    name: str
19    address: Address
20
21payload = User(1, "Mina", Address("Toronto", "CA"))
22print(json.dumps(payload, cls=DataclassEncoder, indent=2))

asdict recursively converts nested dataclasses, which keeps the solution compact.

Extend Encoder for Common Non-JSON Types

Real payloads often include additional Python types. Add explicit conversions in the same encoder.

python
1import json
2from dataclasses import dataclass, asdict, is_dataclass
3from datetime import datetime, timezone
4from decimal import Decimal
5from uuid import UUID, uuid4
6
7class AppEncoder(json.JSONEncoder):
8    def default(self, obj):
9        if is_dataclass(obj):
10            return asdict(obj)
11        if isinstance(obj, datetime):
12            return obj.astimezone(timezone.utc).isoformat()
13        if isinstance(obj, UUID):
14            return str(obj)
15        if isinstance(obj, Decimal):
16            return str(obj)
17        return super().default(obj)
18
19@dataclass
20class Invoice:
21    id: UUID
22    amount: Decimal
23    created_at: datetime
24
25invoice = Invoice(uuid4(), Decimal("19.95"), datetime.now(timezone.utc))
26print(json.dumps(invoice, cls=AppEncoder, indent=2))

Converting Decimal to string helps avoid precision loss in financial values.

Decoding Back Into Dataclasses

Serialization is half the workflow. For decoding, parse JSON then construct dataclass explicitly.

python
1import json
2from dataclasses import dataclass
3from datetime import datetime
4from decimal import Decimal
5from uuid import UUID
6
7@dataclass
8class Invoice:
9    id: UUID
10    amount: Decimal
11    created_at: datetime
12
13
14def load_invoice(raw: str) -> Invoice:
15    data = json.loads(raw)
16    return Invoice(
17        id=UUID(data["id"]),
18        amount=Decimal(data["amount"]),
19        created_at=datetime.fromisoformat(data["created_at"])
20    )

Explicit loader logic gives clearer errors than implicit dynamic mapping.

Keep Serialization Rules Centralized

In larger codebases, scattered asdict calls create inconsistent output and hidden schema drift. A better pattern is one serialization module with:

  • Encoder class.
  • Decode helpers.
  • Round-trip tests.
  • Version notes for schema changes.

This keeps API and event payload formats stable across services.

Testing Round-Trip Safety

Add tests that validate encode and decode behavior for representative models.

python
1def test_invoice_round_trip():
2    raw = '{"id":"12345678-1234-5678-1234-567812345678","amount":"29.50","created_at":"2026-03-04T10:00:00+00:00"}'
3    inv = load_invoice(raw)
4    out = json.dumps(inv, cls=AppEncoder)
5    assert "29.50" in out

Round-trip tests catch accidental changes in field names and formats.

Common Pitfalls

  • Calling json.dumps directly on dataclass objects. Fix by using a custom encoder or explicit asdict conversion.
  • Converting Decimal to float. Fix by serializing as string when precision matters.
  • Mixing timezone-naive and timezone-aware datetime values. Fix by normalizing datetime policy before encoding.
  • Spreading conversion rules across many files. Fix by centralizing serialization logic.
  • Assuming decode can be automatic for complex types. Fix by writing explicit loader functions with validation.

Summary

  • Dataclasses are not JSON-serializable by default in Python.
  • A custom JSONEncoder with dataclass handling is the most maintainable approach.
  • Extend one encoder for common custom types to keep output consistent.
  • Use explicit decode helpers for predictable type reconstruction.
  • Protect your schema with round-trip and compatibility tests.

Course illustration
Course illustration

All Rights Reserved.