How to Convert JSON to Custom Objects in Python
Working with raw dictionaries (data['user']['name']) is error-prone. For example, typos go unnoticed, IDEs can't provide autocompletion, and there's no type validation. Converting JSON to custom objects (data.user.name) enables better tooling, type safety, and cleaner code.
This guide covers approaches from simple namespace conversion to full validation with Pydantic.
Using SimpleNamespace (Quick and Easy)
For dot-notation access without defining a schema:
import json
from types import SimpleNamespace
json_str = '{"name": "Alice", "id": 100, "meta": {"active": true}}'
# object_hook converts every dict to SimpleNamespace recursively
data = json.loads(json_str, object_hook=lambda d: SimpleNamespace(**d))
print(data.name) # Alice
print(data.id) # 100
print(data.meta.active) # True
# Access attributes
print(type(data)) # <class 'types.SimpleNamespace'>
Output:
Alice
100
True
<class 'types.SimpleNamespace'>
Reusable Parsing Function
import json
from types import SimpleNamespace
def json_to_object(json_string):
"""Convert JSON string to object with dot notation."""
return json.loads(json_string, object_hook=lambda d: SimpleNamespace(**d))
def json_file_to_object(file_path):
"""Load JSON file as object with dot notation."""
with open(file_path) as f:
return json.load(f, object_hook=lambda d: SimpleNamespace(**d))
# Usage
config = json_file_to_object("config.json")
print(config.database.host)
SimpleNamespace provides no validation, no type hints, and no IDE autocompletion for specific fields. It's best for quick scripts or exploring unknown JSON structures.
Using Dataclasses (Structured Schema)
For production code, define explicit structures with type hints:
import json
from dataclasses import dataclass
from typing import Optional
@dataclass
class Address:
city: str
zip_code: str
@dataclass
class User:
name: str
id: int
email: Optional[str] = None
active: bool = True
json_str = '{"name": "Bob", "id": 50, "email": "bob@example.com"}'
# Parse and unpack
raw_dict = json.loads(json_str)
user = User(**raw_dict)
print(user) # User(name='Bob', id=50, email='bob@example.com', active=True)
print(user.name) # Bob
print(user.email) # bob@example.com
Output:
User(name='Bob', id=50, email='bob@example.com', active=True)
Bob
bob@example.com
Handling Nested Objects
import json
from dataclasses import dataclass
from typing import Optional
@dataclass
class Address:
street: str
city: str
country: str = "USA"
@dataclass
class User:
name: str
id: int
address: Address
def dict_to_user(data: dict) -> User:
"""Convert nested dict to User with Address."""
address_data = data.pop('address')
address = Address(**address_data)
return User(**data, address=address)
json_str = '''
{
"name": "Alice",
"id": 1,
"address": {
"street": "123 Main St",
"city": "Boston"
}
}
'''
raw = json.loads(json_str)
user = dict_to_user(raw)
print(user.address.city) # Boston
Generic Dataclass Parser
from dataclasses import dataclass, fields, is_dataclass
from typing import get_type_hints, get_origin, get_args
import json
def from_dict(cls, data):
"""Recursively convert dict to dataclass instance."""
if not is_dataclass(cls):
return data
fieldtypes = get_type_hints(cls)
kwargs = {}
for field in fields(cls):
value = data.get(field.name)
field_type = fieldtypes[field.name]
if value is not None and is_dataclass(field_type):
kwargs[field.name] = from_dict(field_type, value)
elif value is not None and get_origin(field_type) is list:
item_type = get_args(field_type)[0]
if is_dataclass(item_type):
kwargs[field.name] = [from_dict(item_type, item) for item in value]
else:
kwargs[field.name] = value
else:
kwargs[field.name] = value
return cls(**kwargs)
# Usage
@dataclass
class Order:
id: int
product: str
@dataclass
class Customer:
name: str
orders: list[Order]
json_str = '''
{
"name": "Alice",
"orders": [
{"id": 1, "product": "Book"},
{"id": 2, "product": "Laptop"}
]
}
'''
customer = from_dict(Customer, json.loads(json_str))
print(customer.orders[0].product) # Book
Using Pydantic (Full Validation)
Pydantic provides automatic type conversion and validation, making it ideal for APIs:
pip install pydantic
from pydantic import BaseModel, EmailStr, validator
from typing import Optional
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
name: str
age: int
email: EmailStr
address: Optional[Address] = None
tags: list[str] = []
@validator('age')
def age_must_be_positive(cls, v):
if v < 0:
raise ValueError('Age must be positive')
return v
# Pydantic handles type conversion automatically
json_str = '''
{
"name": "Alice",
"age": "25",
"email": "alice@example.com",
"address": {"street": "123 Main", "city": "NYC", "zip_code": "10001"}
}
'''
user = User.parse_raw(json_str)
print(user.age) # 25 (converted from string to int)
print(type(user.age)) # <class 'int'>
print(user.address.city) # NYC
Pydantic V2 Syntax
from pydantic import BaseModel, field_validator
from typing import Optional
class Product(BaseModel):
name: str
price: float
in_stock: bool = True
@field_validator('price')
@classmethod
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return round(v, 2)
# Parse from JSON string
product = Product.model_validate_json('{"name": "Widget", "price": 19.999}')
print(product.price) # 20.0 (rounded by validator)
# Parse from dict
product = Product.model_validate({"name": "Gadget", "price": 29.99})
print(product)
Output:
20.0
name='Gadget' price=29.99 in_stock=True
- Automatic type coercion (string "100" → int 100)
- Validation with custom validators
- JSON schema generation
- FastAPI integration
- Excellent error messages
Using attrs Library
An alternative to dataclasses with additional features:
pip install attrs
import attr
from typing import Optional
@attr.s
class User:
name: str = attr.ib()
id: int = attr.ib()
email: Optional[str] = attr.ib(default=None)
@email.validator
def check_email(self, attribute, value):
if value and '@' not in value:
raise ValueError("Invalid email")
u = User(name="Alice", id=1, email="alice@example.com")
print(u)
Output:
User(name='Alice', id=1, email='alice@example.com')
Converting Back to JSON
from dataclasses import dataclass, asdict
import json
@dataclass
class User:
name: str
id: int
user = User(name="Alice", id=1)
# Dataclass to JSON
json_str = json.dumps(asdict(user))
print(json_str) # {"name": "Alice", "id": 1}
# Pydantic to JSON
from pydantic import BaseModel
class PydanticUser(BaseModel):
name: str
id: int
p_user = PydanticUser(name="Bob", id=2)
json_str = p_user.model_dump_json() # Pydantic V2
print(json_str) # {"name":"Bob","id":2}
Output:
{"name": "Alice", "id": 1}
{"name":"Bob","id":2}
Summary
| Method | Type Safety | Validation | Auto-Convert | Best For |
|---|---|---|---|---|
| Dict | None | None | No | Simple scripts |
| SimpleNamespace | None | None | No | Quick exploration |
| Dataclass | Hints only | Manual | No | Internal logic |
| Pydantic | Full | Built-in | Yes | APIs, validation |
| attrs | Configurable | Built-in | Optional | Flexible schemas |
- Use SimpleNamespace for quick scripts and exploring unknown JSON
- Use Dataclasses for internal application models with type hints
- Use Pydantic for API development, external data, and when validation is critical
Avoid raw dictionary access in business logic: the upfront cost of defining a schema pays off in maintainability and fewer runtime errors.