Python Parsing, Serialization, and Strict Mode
Goals
Choose load/loads/dump/dumps correctly, read and write UTF-8 files while preserving parse-error locations, understand that Python’s standard-library defaults accept extensions such as duplicate keys and non-standard numbers, and explicitly tighten input and output to an interface contract.
Prepare a minimal environment
The standard-library json module needs no installation. The integrated project additionally uses a JSON Schema validator, so create an isolated environment before installing the pinned dependency. Run these commands from the project root that contains both docs-EN/ and .website/:
Push-Location -LiteralPath 'docs-EN\json'
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r .\examples\requirements.txt
Pop-LocationDo not add .venv to the repository. This knowledge base was verified with Python 3.11.9; the code also supports Python 3.10+ supported by jsonschema 4.26.0. Re-run the tests when using other versions.
The four functions differ by “string or file object”
| Direction | String/bytes | An open file object |
|---|---|---|
| JSON → Python | json.loads(text) | json.load(file) |
| Python → JSON | json.dumps(value) | json.dump(value, file) |
Remember s as “string.” load and dump accept file-like objects with read or write; they do not directly accept a Path.
import json
text = '{"name": "agent", "max_steps": 5}'
value = json.loads(text)
round_trip = json.dumps(value, ensure_ascii=False)
assert value["name"] == "agent"
assert json.loads(round_trip) == valueThe assert statements here only illustrate equivalence and should not replace project tests; python -O removes bare assertions.
Catch syntax errors while preserving their location
import json
text = '{\n "name": "agent",\n}'
try:
config = json.loads(text)
except json.JSONDecodeError as error:
print(f"invalid JSON at line {error.lineno}, column {error.colno}")
else:
print(config)Production logs should not casually print the whole text: a payload can contain tokens, personal information, or prompt-injection text. Record stable error codes, safe paths, line/column positions, request IDs, and data sources instead of whole values.
Read and write UTF-8 files
import json
from pathlib import Path
path = Path("agent_config.json")
data = {"name": "meeting assistant", "enabled": True}
with path.open("w", encoding="utf-8", newline="\n") as file:
json.dump(
data,
file,
ensure_ascii=False,
allow_nan=False,
indent=2,
sort_keys=True,
)
file.write("\n")
with path.open("r", encoding="utf-8") as file:
loaded = json.load(file)encoding="utf-8"does not depend on the current Windows code page.ensure_ascii=Falsekeeps non-ASCII text readable without changing the logical string.allow_nan=Falseprevents encoding the RFC 8259-forbiddenNaN/Infinityvalues.indent=2is for people; compact output can be appropriate for network payloads.sort_keys=Truecan stabilize ordinary test output, but is not cryptographic canonicalization.- A final LF helps diffs and is not part of the JSON value.
Python defaults are broader than RFC 8259
As documented for Python 3.14.6, the standard library has these compatibility behaviors by default:
import json
import math
duplicate = json.loads('{"role":"reader","role":"admin"}')
non_standard = json.loads("NaN")
assert duplicate == {"role": "admin"}
assert math.isnan(non_standard)This does not make duplicate keys or NaN standard JSON. If the parsing layer silently drops the first key, a Schema can no longer discover the conflict; duplicate authorization fields are particularly dangerous.
Use hooks to reject duplicate keys and non-standard constants
import json
from typing import Any
def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate object member")
result[key] = value
return result
def reject_constant(_: str) -> None:
raise ValueError("non-standard numeric literal")
value = json.loads(
'{"enabled": true}',
object_pairs_hook=unique_object,
parse_constant=reject_constant,
)This is still not a complete security boundary: the default float can turn 1e9999 into positive infinity; lone UTF-16 surrogates, excessively deep nesting, huge arrays, and large files also need extra checks. The project’s strict_json.py centralizes these rules behind a testable entry point.
Numeric precision and Decimal
import json
from decimal import Decimal
data = json.loads('{"price": 0.1}', parse_float=Decimal)
assert data["price"] == Decimal("0.1")Decimal changes only the local parsing result. It is not a JSON-native type; an output contract still needs to specify a string, an integer number of smallest currency units, or another explicit structure. Very large integer IDs crossing JavaScript systems should usually be strings as well.
Encode non-native Python types explicitly
datetime, Decimal, UUID, Path, set, and custom classes are not JSON-native values. Do not write a fallback encoder that applies str(obj) to every unknown object, because type errors will silently become strings. Define the field contract first, then use a narrow conversion:
from datetime import datetime, timezone
import json
payload = {
"created_at": datetime.now(timezone.utc).isoformat(),
"tags": sorted({"json", "agent"}),
}
text = json.dumps(payload, ensure_ascii=False, allow_nan=False)Time strings also need an agreed time zone, precision, and parsing rule; “serializable” does not mean both sides understand the same thing.
Double encoding
import json
payload = {"name": "agent"}
once = json.dumps(payload)
twice = json.dumps(once)
print(once) # {"name": "agent"}
print(twice) # "{\"name\": \"agent\"}"The second result is a JSON string value, not an object. If an HTTP client provides json=payload, normally pass it a Python object; when using data=, you must handle encoding and media type yourself, subject to the client documentation.
Quick command-line check
Python 3.11 can use:
Push-Location -LiteralPath 'docs-EN\json'
python -m json.tool .\examples\agent_config.json
Pop-LocationPython 3.14 adds the python -m json entry point, but the course continues to use json.tool for compatibility with this project environment. It checks syntax and formats output; it does not replace Schema or business validation.
Common errors and troubleshooting
- Parsing JSON with
eval: untrusted text crosses into code execution; use a data parser only. - Setting
allow_nan=Falseonly at output: input also needsparse_constantand a post-parse finite-number check. - Using
skipkeys=Trueto hide non-string keys: it loses data; the contract should explicitly reject them. - Calling
json.dumprepeatedly on the same file: JSON has no record framing, so the result is not a valid single document. - Catching
Exceptionand printing only “parsing failed”: preserve safe error type, position, and handling action.
Exercises
- Parse text with
loads, read a file containing the same text withload, and assert that their results match. - Input a trailing comma, empty text, and an unterminated string separately, then record
linenoandcolno. - Encode
float("nan")withallow_nan=Falseand explain which layer raises the exception. - Implement the duplicate-key hook above and write one failing test each for a top-level and nested object.
- Explain why
json.loads(json.dumps({1: "x"}))does not equal the original dictionary.
Self-test
- What is the input difference between
loadandloads? - Does
ensure_ascii=Falsechange a string’s logical content? - Is
sort_keys=Trueenough to support a digital signature? - Why cannot a Schema repair duplicate keys already lost during parsing?
- Can
parse_constantalone reject the infinity produced by1e9999?
Summary and next step
The standard library provides mechanisms; the application chooses a profile. The next lesson extends strict boundaries to Unicode, numeric ranges, object order, and resource exhaustion: Interoperability, Unicode, and Number Boundaries. Return to the JSON Learning Index.
References
Source review date: 2026-07-22.