JSON is everywhere — REST APIs, config files, database query results, message queues, webhook payloads. Python makes basic parsing trivial, but production JSON handling requires knowing when the stdlib is enough, when you need validation, and when you need a faster or streaming parser. In 2026, the tooling is excellent — here is how to use it correctly.
What changed in 2026
- Pydantic v2 is the de facto standard for JSON validation — the Rust-backed rewrite from 2023 is now universally adopted; avoid Pydantic v1 in new code.
orjson became the performance default — FastAPI, many ORMs, and internal tooling at major companies switched to it for its 3–10x speedup over stdlib.
- Python 3.13 stdlib
json gained minor speed improvements but remains slower than orjson for large payloads.
- JSON Lines (ndjson) is the dominant streaming format — bulk API exports, log pipelines, and LLM output streaming all use newline-delimited JSON.
stdlib json — the baseline
import json
# Parse a string
text = '{"name": "Alice", "age": 30, "tags": ["python", "data"]}'
data = json.loads(text)
print(data["name"]) # Alice
print(data["tags"]) # ['python', 'data']
# Parse from a file
with open("config.json") as f:
config = json.load(f)
# Serialize to string
output = json.dumps(data, indent=2, ensure_ascii=False)
# Serialize to file
with open("output.json", "w") as f:
json.dump(data, f, indent=2)
Validation with Pydantic v2 — the safe approach
from pydantic import BaseModel, ValidationError
from typing import Annotated
from pydantic import Field
import json
class UserEvent(BaseModel):
user_id: str
action: str
score: Annotated[float, Field(ge=0, le=100)]
tags: list[str] = []
raw = '{"user_id": "u123", "action": "click", "score": 85.5}'
# Parse and validate in one call
try:
event = UserEvent.model_validate_json(raw)
print(event.score) # 85.5 — typed, validated
except ValidationError as e:
print(e.errors()) # structured error list
# From a dict
event = UserEvent.model_validate({"user_id": "u123", "action": "view", "score": 40})
Pydantic v2 rejects extra fields (configurable), coerces compatible types, and raises structured errors — much safer than raw dict access.
orjson — when speed matters
import orjson
# orjson.loads / orjson.dumps are drop-in replacements
data = orjson.loads('{"ts": "2026-06-01T10:00:00Z", "value": 3.14}')
# Serialize — returns bytes, not str
payload: bytes = orjson.dumps(
data,
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
)
# orjson natively handles datetime objects
from datetime import datetime
orjson.dumps({"ts": datetime.utcnow()})
# b'{"ts":"2026-06-03T10:00:00"}' — no TypeError like stdlib
Performance comparison
| Library |
Parse 1MB JSON |
Serialize 1MB |
Notes |
stdlib json |
~25 ms |
~30 ms |
No deps, always available |
orjson |
~4 ms |
~5 ms |
5–7x faster, returns bytes |
ujson |
~8 ms |
~8 ms |
3x faster, less maintained |
msgspec |
~3 ms |
~4 ms |
Also does MessagePack; schema-typed |
Use stdlib for config files and occasional parsing. Use orjson in API handlers and batch processing loops.
Streaming large JSON files with ijson
import ijson
# Parse a 500MB JSON array without loading it all into memory
with open("large_events.json", "rb") as f:
for event in ijson.items(f, "events.item"): # "events.item" = each item in events[]
process(event) # handle one event at a time; memory stays constant
# JSON Lines (ndjson) — even simpler, one object per line
def parse_jsonl(path: str):
with open(path) as f:
for line in f:
line = line.strip()
if line:
yield json.loads(line)
for record in parse_jsonl("output.jsonl"):
process(record)
Handling malformed JSON safely
def safe_parse(text: str) -> dict | None:
try:
return json.loads(text)
except json.JSONDecodeError as e:
logger.warning("JSON parse error at position %d: %s", e.pos, e.msg)
return None
# For LLM outputs that embed JSON in markdown fences
import re
def extract_json_from_llm(text: str) -> dict | None:
match = re.search(r"```(?:json)?\s*([\s\S]+?)\s*```", text)
if match:
return safe_parse(match.group(1))
return safe_parse(text) # try raw if no fence
How to pick the right approach
- Simple config or small API response? stdlib
json — zero dependencies.
- API input that needs validation and typed access? Pydantic v2
model_validate_json.
- Hot path serialization (API responses, batch jobs)?
orjson.
- Large JSON files (>50MB)?
ijson for arrays, or JSONL with line-by-line parsing.
- LLM output that might embed JSON in markdown? Extract with regex before parsing.
Common mistakes
KeyError on missing keys. Use .get("key", default) or define a Pydantic model. Raw dict access with data["key"] crashes on any API that omits optional fields.
Float precision issues. JSON floats are IEEE 754. For financial data, use decimal.Decimal with parse_float=Decimal in json.loads.
Loading huge files with json.load() — this reads the entire file into RAM. A 1GB JSON file needs ~3–4GB of RAM after parsing. Use ijson or convert to JSONL.
Serializing non-serializable types. datetime, UUID, Decimal, and custom objects cause TypeError in stdlib. Use orjson (handles most automatically) or write a custom encoder.
Using eval() to parse JSON. This executes arbitrary Python code. It is a critical security vulnerability if the input is user-supplied. Always use json.loads.
What to skip
simplejson — it was useful before Python 3's stdlib json improved; no reason to use it today.
- Manual string manipulation to build JSON — always use
json.dumps or orjson.dumps; manual building produces invalid JSON with escaped quotes.
json.loads on very large files — use streaming; the stdlib function loads everything into memory at once.
FAQ
When should I use JSON vs MessagePack or Protobuf?
JSON for any human-readable config, API, or debug output. MessagePack (via msgpack) for internal binary protocols where size/speed matters. Protobuf for cross-language typed schemas with schema evolution guarantees.
How do I pretty-print JSON in the terminal?
python -m json.tool < file.json in any Python 3 installation, or cat file.json | jq . with jq installed.
How do I handle JSON with comments (JSONC)?
Use the jsonc package or strip comments with a regex before parsing. Standard JSON does not allow comments — JSONC is a Microsoft extension used in VS Code configs.
How do I convert a Python dataclass to JSON?
Use dataclasses.asdict(obj) then json.dumps, or switch to msgspec.Struct which serializes natively without the intermediate dict.
Where to go next