Python JSON decoding converts a document into Python values, but a successful json.loads call does not establish an application schema, authorization or a safe resource budget. This guide explains duplicate keys, special numeric values, decoder hooks and bounded input, with five local observations you can reproduce.
This guide explains size limits, duplicate keys, numbers, and validation so JSON does not become a false trust signal. The goal is a predictable application contract rather than accepting every value the standard library can represent.
Python JSON parsing: bound the input before json.loads
Apply an appropriate request or file-size limit before loading the entire document. An oversized body can consume memory and CPU even if the application later rejects its fields. Check the real transport path, not only a Content-Length value supplied by the sender.
Include decompressed size where compressed requests are allowed. A small compressed body can expand significantly. The application’s limit should cover the representation actually passed to the decoder.
Set time and resource budgets at relevant service boundaries. The JSON module is not a complete denial-of-service prevention layer. A parser call inside an otherwise unlimited worker can still occupy valuable capacity.
Decide which top-level values are allowed
JSON can represent objects, arrays, strings, numbers, booleans, and null. An API expecting an object should explicitly reject a valid top-level list or scalar instead of letting later field access fail unpredictably.
Validate required keys, unknown-key policy, and field types. Keep the schema close to the task: a configuration document and an event message need different contracts. Syntax acceptance alone cannot answer whether the document fits either one.
Do not rely on truthiness for required numeric or boolean values. Zero and false can be legitimate inputs. Distinguish missing, null, and an intentionally provided value through explicit validation.
Handle duplicate object names deliberately
Repeated object names can produce different interpretations across parsers and application layers. Python’s default behavior accepts repeated names with documented handling, which may not match an upstream validator’s assumption.
For a strict contract, use a supported decoding hook to detect duplicate pairs and reject them where appropriate. Test nested objects too. A rule applied only to the top level can leave ambiguity inside important fields.
Do not sign, authorize, or compare a document using one interpretation and then execute another. Parser differences can matter when a gateway, verifier, and application each handle the same body.
Review numeric range and precision
The decoder’s number representation may not match the application’s needs. Monetary values, identifiers, and scientific measurements have different precision and range requirements. Define those requirements before arithmetic or database insertion.
Use an appropriate parse strategy when decimal precision is required and validate the resulting values. Converting an already rounded floating-point value later does not restore the original decimal input.
Check integer size and relevant runtime limits rather than assuming arbitrary numeric text is harmless. A syntactically valid number can be outside the supported domain or expensive to process in downstream operations.
Reject unsupported special values
The Python JSON implementation documents behavior for values such as NaN and Infinity that are not part of strict interoperable JSON. Decide whether the application permits them; many business schemas should not.
Use the supported decoder controls to reject nonstandard constants when a strict contract requires it. Also configure serialization deliberately so the application does not emit values another system cannot accept.
Test the actual provider and client path. A document that round-trips through one Python program may still violate the receiving system’s expectations. Interoperability needs a shared contract, not merely local success.
Validate depth, strings, and aggregate work
Large nesting, long strings, and huge collections can stress the parser or later application logic. The standard library documentation warns about resource consumption and does not supply every application-specific limit.
Apply the relevant bounded-input design and validate structural complexity where required. Consider the cost of downstream validation and transformation, not only parsing. A thousand records can each trigger expensive additional work.
Reject or constrain oversized fields before logging, rendering, or constructing queries. Parameterized SQL and context-aware output handling remain necessary even when the original data arrived as JSON.
Keep trust decisions outside the decoder
Parsed fields are still attacker-controlled when the source is untrusted. Verify identity, tenant scope, object permissions, and allowed actions through trusted application state. A role field in the document does not grant the sender that role.
Avoid turning decoded keys directly into unrestricted function calls, class construction, or filesystem paths. Choose explicit allowed operations and validated arguments. JSON is a data format, not an authority mechanism.
If signatures protect the body, follow the documented canonicalization or exact-byte verification protocol. Re-encoding a parsed object can change spacing, ordering, and numeric representation. A locally serialized substitute may not be the signed content.
Return controlled errors and preserve evidence
Report invalid input with a useful but nonsecret error category. Do not echo the entire request body into public responses or broad logs. A parse failure can contain credentials or private customer data.
Record controlled request identifiers, size, and validation category where useful. Preserve detailed evidence only through the approved diagnostic path. Resource-limit failures deserve monitoring separately from ordinary missing fields.
Avoid retrying the same malformed document indefinitely. A deterministic schema error needs correction, not an automated retry storm. Make the client-facing response and server handling consistent.
Test the complete application contract
Build cases for oversized input, compressed expansion, duplicate keys, invalid encoding, special numbers, deep structures, missing fields, and unauthorized object identifiers. Verify the denial path without exposing sensitive content.
Test valid boundary values as well. A strict validator should not reject legitimate zero, false, empty optional collections, or supported Unicode merely because they look unusual. Precision and normalization need task-specific decisions.
For an event endpoint, bound the body, verify the event’s authenticity, parse under a strict policy, validate its schema, and authorize the target record. Each stage answers a different question and should have its own clear failure behavior.
Reproduce five Python JSON decoder-policy checks
The following 5 observations were reproduced with CPython 3.13.14 on Linux using isolated local sample data, not the website server or production information. Review the script before running it in a controlled local environment.
from decimal import Decimal
import json
import math
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate key")
result[key] = value
return result
def reject_constant(value):
raise ValueError("unsupported constant")
def rejected(call):
try:
call()
except ValueError as error:
return type(error).__name__
return "accepted"
results = {
"default_duplicate_value": json.loads('{"x": 1, "x": 2}')["x"],
"duplicate_hook": rejected(lambda: json.loads('{"x": 1, "x": 2}', object_pairs_hook=unique_object)),
"default_nan": math.isnan(json.loads("NaN")),
"constant_hook": rejected(lambda: json.loads("NaN", parse_constant=reject_constant)),
"decimal_type": type(json.loads("1.25", parse_float=Decimal)).__name__,
}
print(json.dumps(results, indent=2))
Observed output:
{
"default_duplicate_value": 2,
"duplicate_hook": "ValueError",
"default_nan": true,
"constant_hook": "ValueError",
"decimal_type": "Decimal"
}
The default decoder keeps the last repeated x value and accepts NaN as a floating-point value. The demonstrated hooks reject those two inputs with ValueError, and parse_float=Decimal produces a Decimal for the numeric example. These are explicit decoder choices rather than guarantees provided by every JSON consumer.
The hooks do not cap body size, nesting depth, total object count or processing time. They do not validate business fields or authorize an object identifier. Bound input before decoding and validate the accepted data afterward; use representative negative cases across the complete application path.
Reproduce the examples: Download the six Python practice scripts (ZIP, pack 2), including this script and recorded results. No signup is required. The README records runtime and platform limits. See the Python 3.13 json reference for documented semantics, and continue with Decimal construction and rounding rules.
Frequently asked questions
Does valid JSON mean trusted input?
No. Authorization, schema, and resource checks remain application responsibilities.
Are duplicate keys universally interpreted the same way?
No. Define and enforce one policy across the processing path.
Where can I check decoder hooks and limitations?
Read the Python JSON reference before choosing parsing and serialization options.
For a complementary workflow, read AI Structured Outputs: 7 Checks for Reliable Application Data.