Python JSON Serialization Guide

Python ~5 min read
PythonJSONSerializationjson modulePerformance

In-depth guide to the Python json module, covering json.dumps, json.loads, custom encoders, datetime handling, and dataclass integration.

Data Type Mapping

Python’s json module supports the following core type mapping. Understanding this table is the foundation for correct serialization usage.

Python TypeJSON TypeNotes
dictobjectKeys must be strings
list, tuplearrayTuple becomes list after deserialization
strstringAutomatic Unicode escaping
int, floatnumberNaN/Infinity need special handling
True, Falsetrue, falseCase-sensitive
NonenullMaps 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}")

JSON API Comparison Across Languages

Language Library Serialize Deserialize Pretty Print Config
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions
TypeScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Rust serde_json serde_json::to_string(&obj) serde_json::from_str::<T>(str) serde_json::to_string_pretty(&obj) serde attributes, custom (de)serializers

Frequently Asked Questions (FAQ)

How to handle datetime types in JSON serialization?

Most language JSON libraries do not support datetime types by default. The common approach is to serialize as ISO 8601 format strings (e.g. "2026-07-15T10:30:00Z") or Unix timestamps, then convert back to datetime objects during deserialization.

How to ignore null values or empty fields in JSON serialization?

Different languages have different implementations: Python can filter None values in custom encoders; JavaScript can use replacer functions; Java Jackson uses @JsonInclude annotations; Go uses omitempty struct tags; C# sets DefaultIgnoreCondition.

How to handle circular references in JSON serialization?

Circular references are a common pitfall. Standard JSON libraries usually throw exceptions or cause stack overflow. Solutions include using @JsonIdentityInfo (Java Jackson), implementing custom serializers, or converting to DTOs before serialization.

Why are my private fields not being serialized?

JSON serialization libraries typically only serialize public fields or properties with getters. This is a security consideration. You can use annotations like @JsonProperty (Java Jackson) or [JsonInclude] (C#) to include non-public members.

What are the performance optimization tips for JSON serialization?

1) Reuse serializer instances; 2) Use streaming APIs for large data; 3) Use compile-time generation (C# Source Generator, Go easyjson); 4) Avoid excessive nesting; 5) Use database pagination for large JSON.

Recommended Tools

These tools can help you handle JSON data more efficiently in Python development:

Related Articles