Skip to content

Custom Decoders (object_hook)

Use object_hook to transform JSON objects during deserialization.

Mental Model

By default, json.loads turns every JSON object into a plain Python dict. An object_hook intercepts each dict as it is created and lets you return something richer — a dataclass, a namedtuple, or a domain object. Think of it as a post-processing filter on the JSON parser's output, converting raw dicts into typed objects automatically.

Basic object_hook Usage

Transform JSON objects with a custom function.

```python import json from datetime import datetime

def parse_date(obj): if 'date' in obj and isinstance(obj['date'], str): obj['date'] = datetime.fromisoformat(obj['date']) return obj

json_str = '{"event": "Meeting", "date": "2024-12-25T14:30:00"}' data = json.loads(json_str, object_hook=parse_date) print(data) print(f"Date type: {type(data['date'])}") ```

{'event': 'Meeting', 'date': datetime.datetime(2024, 12, 25, 14, 30)} Date type: <class 'datetime.datetime'>

Creating Custom Classes from JSON

Convert JSON objects to custom Python classes.

```python import json from dataclasses import dataclass

@dataclass class User: name: str age: int

def user_decoder(obj): if 'name' in obj and 'age' in obj: return User(obj['name'], obj['age']) return obj

json_str = '{"name": "Alice", "age": 30}' user = json.loads(json_str, object_hook=user_decoder) print(user) print(f"Type: {type(user)}") ```

User(name='Alice', age=30) Type: <class '__main__.User'>


Exercises

Exercise 1. Write a custom object_hook function that converts JSON objects containing a "_type": "date" field into datetime.date objects. The JSON object will also have "year", "month", and "day" fields. Parse the string '{"_type": "date", "year": 2024, "month": 12, "day": 25}' and verify the result is a date object.

Solution to Exercise 1

```python import json from datetime import date

def date_decoder(obj): if obj.get("_type") == "date": return date(obj["year"], obj["month"], obj["day"]) return obj

json_str = '{"_type": "date", "year": 2024, "month": 12, "day": 25}' result = json.loads(json_str, object_hook=date_decoder) print(result) # 2024-12-25 print(type(result)) # ```


Exercise 2. Write a custom object_hook that converts JSON objects with a "_type": "complex" field into Python complex numbers, using "real" and "imag" fields. For example, '{"_type": "complex", "real": 3, "imag": 4}' should decode to (3+4j).

Solution to Exercise 2

```python import json

def complex_decoder(obj): if obj.get("_type") == "complex": return complex(obj["real"], obj["imag"]) return obj

json_str = '{"_type": "complex", "real": 3, "imag": 4}' result = json.loads(json_str, object_hook=complex_decoder) print(result) # (3+4j) print(type(result)) # ```


Exercise 3. Write a decoder that handles nested custom types: a "_type": "point" with "x" and "y" fields, and a "_type": "line" with "start" and "end" fields (each being a point). Parse a JSON string representing a line and verify the structure.

Solution to Exercise 3

```python import json from collections import namedtuple

Point = namedtuple("Point", ["x", "y"]) Line = namedtuple("Line", ["start", "end"])

def geometry_decoder(obj): if obj.get("_type") == "point": return Point(obj["x"], obj["y"]) if obj.get("_type") == "line": return Line(obj["start"], obj["end"]) return obj

json_str = ''' { "_type": "line", "start": {"_type": "point", "x": 0, "y": 0}, "end": {"_type": "point", "x": 5, "y": 10} } ''' result = json.loads(json_str, object_hook=geometry_decoder) print(result) # Line(start=Point(x=0, y=0), end=Point(x=5, y=10)) print(result.start.x) # 0 print(result.end.y) # 10 ```