Data Type Mapping
Python’s json module supports the following core type mapping. Understanding this table is the foundation for correct serialization usage.
| Python Type | JSON Type | Notes |
|---|---|---|
dict | object | Keys must be strings |
list, tuple | array | Tuple becomes list after deserialization |
str | string | Automatic Unicode escaping |
int, float | number | NaN/Infinity need special handling |
True, False | true, false | Case-sensitive |
None | null | Maps to JavaScript null |
Note that tuple is serialized as a JSON array and becomes list after deserialization — this is an irreversible conversion. For custom types like datetime, Decimal, set, frozenset, bytes, and complex, the json module will raise TypeError by default.
json.dumps() Core Parameters
json.dumps() is the most commonly used serialization function in Python, converting Python objects to JSON-formatted strings. Mastering its key parameters can significantly improve code readability and maintainability.
import json
data = {"name": "Alice", "city": "New York", "score": 95.5, "tags": ["Python", "JSON"]}
# Default serialization: compact output, ASCII-escaped
compact = json.dumps(data)
print(compact) # {"name": "Alice", "city": "New York", ...}
# Pretty output: indent + sorted keys
pretty = json.dumps(data, indent=2, sort_keys=True)
print(pretty)
sort_keys=True sorts key names alphabetically, which is useful for version control and diff comparison. indent controls the indentation level — passing None or 0 outputs a single-line compact format.
json.loads() and object_hook
json.loads() parses a JSON string into a Python object. Through the object_hook parameter, you can execute custom transformation operations on every JSON object during deserialization — this is the core mechanism for type restoration and data validation.
import json
from datetime import datetime
json_str = '{"name": "Alice", "age": 28, "created_at": "2026-07-15T10:30:00Z"}'
def custom_object_hook(dct):
"""Automatically restore ISO date strings to datetime objects"""
for key, value in dct.items():
if isinstance(value, str) and key.endswith("_at"):
dct[key] = datetime.fromisoformat(value.replace("Z", "+00:00"))
return dct
result = json.loads(json_str, object_hook=custom_object_hook)
print(result["created_at"]) # 2026-07-15 10:30:00+00:00
print(type(result["created_at"])) # <class 'datetime.datetime'>
File I/O
When working with JSON files, use the streaming interface json.dump() and json.load() — these functions operate directly on file objects, avoiding loading the entire file data into memory at once.
import json
# Write to file
config = {"version": "3.2", "features": {"logging": True, "cache": True}}
with open("config.json", "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
# Read from file
with open("config.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["features"]["logging"]) # True
Always specify encoding="utf-8" in file operations to avoid encoding issues on different platforms.
The default Parameter
For simple one-off serialization, the default parameter is simpler than subclassing JSONEncoder:
import json
from datetime import datetime
data = {"created": datetime.now(), "tags": {1, 2, 3}}
# Quick conversion using default
result = json.dumps(data, default=str)
# Or with a lambda
result = json.dumps(data, default=lambda o: list(o) if isinstance(o, set) else str(o))
Custom JSONEncoder Subclass
For non-standard types like datetime and Decimal, inherit from JSONEncoder and override the default() method.
import json
from datetime import datetime
from decimal import Decimal
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return list(obj)
if isinstance(obj, complex):
return {"real": obj.real, "imag": obj.imag}
return super().default(obj)
data = {
"created": datetime(2026, 7, 15, 10, 30, 0),
"price": Decimal("19.99"),
"tags": {"python", "json"},
"vector": 3 + 4j
}
result = json.dumps(data, cls=CustomEncoder, ensure_ascii=False)
print(result)
Always call super().default(obj) to ensure unhandled types correctly raise TypeError.
Common Pitfalls
Non-ASCII character escaping, circular references, and special float values are the three most common pitfalls in Python JSON development.
import json
# Pitfall 1: NaN/Infinity are not valid strict JSON
bad_math = {"value": float("nan"), "score": float("inf")}
print(json.dumps(bad_math)) # {"value": NaN, "score": Infinity}
print(json.dumps(bad_math, allow_nan=False)) # Raises ValueError
For production environments, enable allow_nan=False and preprocess abnormal values beforehand, replacing them with null or reasonable sentinel values.
import json
# Pitfall 2: Circular references cause RecursionError
a = {"name": "parent"}
a["self"] = a
try:
json.dumps(a)
except RecursionError:
print("Circular reference detected — use a custom encoder or break the cycle")
# Pitfall 3: ensure_ascii garbles non-ASCII text
data = {"city": "北京", "emoji": "🐍"}
print(json.dumps(data))
# {"city": "\u5317\u4eac", "emoji": "\ud83d\udc0d"}
print(json.dumps(data, ensure_ascii=False))
# {"city": "北京", "emoji": "🐍"}
When serving JSON to web clients, set ensure_ascii=False to produce human-readable output. For circular references, either restructure the data or implement a custom encoder that tracks visited objects with id().
Performance Comparison: json vs ujson vs orjson
The standard library json uses a C-accelerated implementation by default (_json module). The pure-Python fallback only activates if the C extension is unavailable. For high-throughput scenarios, the community provides various C extension implementations.
orjson is typically 5 to 10 times faster than the standard library, while ujson is about 2 to 4 times faster. One major advantage of orjson is that it outputs sorted keys by default and has native support for datetime and UUID without needing custom encoders.
Security Practices
The json module only parses pure JSON structures and will not execute arbitrary Python code — this is much safer than pickle. However, when combined with callback mechanisms like object_hook, you still need to guard against potential risks.
import json
MAX_PAYLOAD_BYTES = 1 * 1024 * 1024 # 1MB
def safe_parse(user_input: str):
# First layer: size limit
if len(user_input.encode("utf-8")) > MAX_PAYLOAD_BYTES:
raise ValueError("Input data too large")
# Second layer: parse error handling
try:
data = json.loads(user_input)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON format at position {e.pos}: {e.msg}") from e
# Third layer: basic structure validation
if not isinstance(data, (dict, list)):
raise ValueError("Only JSON objects or arrays are accepted")
return data
asyncio and JSON
For async applications, use aiofiles for non-blocking file I/O with JSON:
import json
import aiofiles
async def load_config(path: str) -> dict:
async with aiofiles.open(path, 'r', encoding='utf-8') as f:
content = await f.read()
return json.loads(content)
async def save_config(path: str, data: dict):
async with aiofiles.open(path, 'w') as f:
await f.write(json.dumps(data, indent=2))
Dataclass Integration
Python 3.7+ dataclasses work seamlessly with JSON using dataclasses.asdict():
from dataclasses import dataclass, asdict
from typing import List, Optional
import json
@dataclass
class User:
name: str
age: int
tags: Optional[List[str]] = None
user = User(name="Alice", age=30, tags=["python", "json"])
json_str = json.dumps(asdict(user), indent=2)
# Perfect for API responses and config files
For deserialization back to dataclasses, use dacite or marshmallow:
from dacite import from_dict
@dataclass
class Config:
host: str
port: int
debug: bool
data = {"host": "localhost", "port": 8080, "debug": True}
config = from_dict(data_class=Config, data=data)
JSON Schema Validation
Use jsonschema library for runtime validation:
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"age": {"type": "integer", "minimum": 0},
"email": {"type": "string", "format": "email"}
},
"required": ["name", "age"]
}
try:
validate(instance={"name": "Alice", "age": 30}, schema=schema)
print("Valid!")
except ValidationError as e:
print(f"Validation error: {e.message}")
Related Resources
- JSON Best Practices — Universal guidelines for all languages
- JSON Schema Validation — Validate your JSON structures
- JSON Security — Protect against common vulnerabilities
- JSON Formatter — Format and validate your JSON online
- JSON Schema Validator — Test your schemas