Introduction
Python's json.dumps() cannot serialize Enum members by default — it raises TypeError: Object of type Color is not JSON serializable. The fix is to use a custom JSONEncoder that converts enum members to their value (or name), use enum.value explicitly before serialization, or define enums that inherit from str or int alongside Enum (e.g., class Color(str, Enum)) so they are natively JSON-serializable. This article covers all approaches.
The Problem
1import json
2from enum import Enum
3
4class Color(Enum):
5 RED = 1
6 GREEN = 2
7 BLUE = 3
8
9data = {"name": "widget", "color": Color.RED}
10
11json.dumps(data)
12# TypeError: Object of type Color is not JSON serializable
json.dumps() only handles basic Python types (dict, list, str, int, float, bool, None). Enum members are not in this set.
Fix 1: Custom JSONEncoder
1import json
2from enum import Enum
3
4class EnumEncoder(json.JSONEncoder):
5 def default(self, obj):
6 if isinstance(obj, Enum):
7 return obj.value # Serialize as the underlying value
8 return super().default(obj)
9
10class Color(Enum):
11 RED = 1
12 GREEN = 2
13 BLUE = 3
14
15data = {"name": "widget", "color": Color.RED}
16result = json.dumps(data, cls=EnumEncoder)
17print(result) # {"name": "widget", "color": 1}
18
19# Serialize as name instead of value
20class EnumNameEncoder(json.JSONEncoder):
21 def default(self, obj):
22 if isinstance(obj, Enum):
23 return obj.name # "RED" instead of 1
24 return super().default(obj)
25
26result = json.dumps(data, cls=EnumNameEncoder)
27print(result) # {"name": "widget", "color": "RED"}
Override default() in a JSONEncoder subclass to handle enum members. Pass cls=EnumEncoder to json.dumps().
Fix 2: str/int Mixin (Recommended for Python 3.11+)
1from enum import Enum, StrEnum, IntEnum
2import json
3
4# IntEnum — members are also ints
5class Priority(IntEnum):
6 LOW = 1
7 MEDIUM = 2
8 HIGH = 3
9
10# StrEnum (Python 3.11+) — members are also strings
11class Status(StrEnum):
12 ACTIVE = "active"
13 INACTIVE = "inactive"
14 PENDING = "pending"
15
16# Both serialize natively without a custom encoder
17data = {"priority": Priority.HIGH, "status": Status.ACTIVE}
18print(json.dumps(data))
19# {"priority": 3, "status": "active"}
20
21# Pre-3.11 string enum pattern
22class Color(str, Enum):
23 RED = "red"
24 GREEN = "green"
25 BLUE = "blue"
26
27print(json.dumps({"color": Color.RED}))
28# {"color": "red"}
IntEnum and StrEnum members are instances of int and str respectively, so json.dumps() handles them without a custom encoder.
Fix 3: Using default Parameter
1import json
2from enum import Enum
3
4class Size(Enum):
5 SMALL = "S"
6 MEDIUM = "M"
7 LARGE = "L"
8
9# Lambda as the default handler
10data = {"size": Size.LARGE, "quantity": 5}
11result = json.dumps(data, default=lambda obj: obj.value if isinstance(obj, Enum) else str(obj))
12print(result) # {"size": "L", "quantity": 5}
13
14# More robust default handler
15def json_default(obj):
16 if isinstance(obj, Enum):
17 return obj.value
18 if hasattr(obj, "__dict__"):
19 return obj.__dict__
20 return str(obj)
21
22result = json.dumps(data, default=json_default)
The default parameter is simpler than subclassing JSONEncoder when you only need to handle a few types.
Fix 4: Pydantic Models
1from enum import Enum
2from pydantic import BaseModel
3
4class Role(str, Enum):
5 ADMIN = "admin"
6 USER = "user"
7 GUEST = "guest"
8
9class User(BaseModel):
10 name: str
11 role: Role
12
13user = User(name="Alice", role=Role.ADMIN)
14print(user.model_dump_json())
15# {"name": "Alice", "role": "admin"}
16
17# Deserialization works automatically
18data = '{"name": "Bob", "role": "user"}'
19bob = User.model_validate_json(data)
20print(bob.role) # Role.USER
21print(bob.role.value) # "user"
Pydantic handles enum serialization and deserialization automatically when enum members inherit from str or int.
Deserialization (JSON to Enum)
1import json
2from enum import Enum
3
4class Color(Enum):
5 RED = 1
6 GREEN = 2
7 BLUE = 3
8
9# Serialize
10data = {"color": Color.RED}
11json_str = json.dumps(data, default=lambda o: o.value if isinstance(o, Enum) else o)
12print(json_str) # {"color": 1}
13
14# Deserialize — custom object hook
15def decode_with_enums(d):
16 if "color" in d:
17 d["color"] = Color(d["color"])
18 return d
19
20restored = json.loads(json_str, object_hook=decode_with_enums)
21print(restored["color"]) # Color.RED
22print(type(restored["color"])) # <enum 'Color'>
Use object_hook in json.loads() to convert raw values back into enum members during deserialization.
Multiple Enum Types
1import json
2from enum import Enum
3
4class Color(Enum):
5 RED = "red"
6 BLUE = "blue"
7
8class Size(Enum):
9 SMALL = "S"
10 LARGE = "L"
11
12# Generic encoder that handles all Enum subclasses
13class EnumEncoder(json.JSONEncoder):
14 def default(self, obj):
15 if isinstance(obj, Enum):
16 return {"__enum__": type(obj).__name__, "value": obj.value}
17 return super().default(obj)
18
19# Decoder that restores enum types
20ENUM_REGISTRY = {"Color": Color, "Size": Size}
21
22def enum_decoder(d):
23 if "__enum__" in d:
24 enum_class = ENUM_REGISTRY[d["__enum__"]]
25 return enum_class(d["value"])
26 return d
27
28data = {"color": Color.RED, "size": Size.LARGE}
29encoded = json.dumps(data, cls=EnumEncoder)
30print(encoded)
31# {"color": {"__enum__": "Color", "value": "red"}, "size": {"__enum__": "Size", "value": "L"}}
32
33decoded = json.loads(encoded, object_hook=enum_decoder)
34print(decoded["color"]) # Color.RED
Common Pitfalls
Using str(enum_member) instead of .value or .name: str(Color.RED) returns "Color.RED", not "RED" or 1. Use .value for the underlying value or .name for the member name.
IntEnum members compare equal to plain ints: Priority.HIGH == 3 is True with IntEnum. This can cause subtle bugs if you rely on type-checking. Use regular Enum if you want strict type separation.
Forgetting deserialization: Serializing enums to JSON is only half the problem. You also need a strategy to convert raw JSON values back to enum members when loading data.
Mixed enum types in one payload: A generic default=lambda o: o.value works for serialization, but deserialization requires knowing which value maps to which enum type. Include type information in the JSON if needed.
StrEnum behavior differences: StrEnum members are actual strings and can be compared with == to plain strings. This is convenient but means isinstance(Status.ACTIVE, str) returns True, which may surprise code that type-checks.
Summary
IntEnum and StrEnum (Python 3.11+) serialize natively with json.dumps()
Custom JSONEncoder with default() handles any enum type
Use .value for the underlying value, .name for the member name
default=lambda o: o.value is the simplest approach for quick serialization
Pydantic models handle enum serialization and deserialization automatically
For round-trip fidelity, include the enum class name in the JSON payload