Todd Mercer

Python JSON Acceptance Test: Duplicate Keys Survive strict=True
json.loads(text, strict=True) does not reject duplicate JSON object names. It accepts them and keeps the last value. To reject duplicates, use object_pairs_hook before the decoder constructs a dictionary. The hook also catches duplicates inside nested objects and names that become identical after JSON escape decoding.
That distinction matters when an agent produces a JSON export or an integration sends a payload you intend to validate. A successful parse proves less than you might expect. By the time an ordinary dictionary reaches your schema validator, an earlier member may already have disappeared.
Python's JSON decoder documentation defines strict around unescaped control characters inside strings, not object-name uniqueness. RFC 8259 recommends unique object names and describes inconsistent receiver behavior when names repeat. Rejecting duplicates is a useful interoperability policy, not a claim that every repeated-name document violates JSON grammar.
What four decoder policies actually accepted
We ran a synthetic acceptance test through Aident Loadout's Code Interpreter Tools on October 1, 2026, using Python 3.13.14. The fixture contained 12 payloads, four policies and five additional assertions. It completed successfully with empty error and stderr. These are observed acceptance results, not a performance benchmark or a production security audit.
Input case | Default loads | Explicit strict=True | Unique-name hook | Consumer policy |
|---|---|---|---|---|
Distinct names, finite values | Accept | Accept | Accept | Accept |
Duplicate name in one object | Accept, last wins | Accept, last wins | Reject | Reject |
Duplicate in a nested object | Accept | Accept | Reject | Reject |
| Accept | Accept | Reject | Reject |
| Accept | Accept | Accept | Accept |
Escaped newline in a string | Accept | Accept | Accept | Accept |
Raw newline inside a string | Reject | Reject | Reject | Reject |
| Accept | Accept | Accept | Reject |
Numeric literal | Accept as infinity | Accept as infinity | Accept as infinity | Reject |
Two JSON documents concatenated | Reject | Reject | Reject | Reject |
The consumer policy combines unique names, rejection of nonfinite literal tokens and a finite-decoded-float check. Those are three separate decisions. If your only requirement is unique names, use the hook column; do not silently adopt the other rules.
Catch the duplicate before building a dictionary
Consider an export that contains the same field twice:
Default loads returns {"id": "008"}. The value 007 is gone. A later type check can confirm that id is a string, but it cannot tell you that two members were present in the original object.
object_pairs_hook receives the ordered name/value pairs for each object. The unique_object function below checks those pairs before constructing the dictionary. It does not scan the whole document for repeated words: [{"id":1},{"id":2}] is allowed because the names belong to different objects.
The escaped-name case is worth keeping in your regression suite. JSON decodes \u0069d to id, so a text search for two literal occurrences of "id" is not enough. The hook checks decoded names. This does not add case folding or Unicode normalization; those would be separate application rules.
Run the complete fixture
This script uses only Python's standard library. Copy it unchanged first. Each row asserts the expected acceptance vector in the same policy order as the table, so a disagreement stops execution rather than quietly producing a reassuring summary.
Expected terminal output includes all_assertions_passed: true, the actual Python version and 12 result rows. Default and explicit strict=True have the same acceptance vector for every tested payload. Unique-name checking rejects the three duplicate-name fixtures without rejecting the separate-object control.
Keep the source payloads alongside the result. A pretty-printed dictionary is not evidence that the original payload had unique names.
Number tokens and float overflow are different checks
RFC 8259's number grammar excludes the literal tokens NaN and Infinity. Python accepts them by default. parse_constant=reject_constant rejects those tokens, including -Infinity.
It does not catch 1e400. That spelling follows JSON number syntax, but the tested Python decoder converts it to an infinite float. The extra assertion deliberately proves that parse_constant alone leaves this case accepted. The recursive finite_floats check then rejects the decoded infinity, including values inside lists and objects.
Rejecting that value is a finite-float consumer policy, not a universal JSON requirement. A consumer that needs wider numeric range or exact decimal arithmetic should choose and test a different number representation. This fixture does not establish precision, financial rounding, integer-size limits or serialization behavior.
Likewise, the final assertion allows the root value 42. Valid JSON need not have an object at its root. If your API requires an object, validate that shape explicitly after decoding.
Repeat the test through a managed Action
If you want your coding agent to run the acceptance matrix in a managed Python execution environment, open Code Interpreter Tools in Aident Loadout. Keep this fixture in the same agent conversation; the catalog link does not automatically upload code or carry a task through login.
If the agent needs setup, use the exact instruction:
Then ask:
The preflight for our synthetic run reported zero credits. That is a dated observation, not a promise that every future Action or execution policy is free. Inspect current inputs, cost and required approval before executing. An integration connection, catalog visit and successful code execution are different outcomes.
For deciding whether a temporary analysis environment or a broader workspace fits the next step, read Code Interpreter versus AI agent sandbox. If the JSON is converted to a text table afterward, use the separate pandas CSV identifier-preservation test. CSV inference can change leading zeros or literal missing-value tokens even after JSON name acceptance is correct.
Scope the acceptance claim
The useful result is narrow: your chosen decoder policy rejects the ambiguity before information is lost. It is not a complete validator for untrusted JSON. Apply payload-size, nesting/resource, expected-shape, allowed-field and domain-value limits appropriate to the receiving application. Keep error logging from exposing sensitive field names or payloads.
We did not test large inputs, resource exhaustion, Unicode normalization, alternate parsers, JavaScript consumers, production requests, exact-decimal arithmetic or a full RFC-conformance suite. The synthetic test contains no customer data. Documentation explains the controls; the native execution establishes only the reported fixture outcomes on Python 3.13.14.
Refresh the test when you change the decoder, Python version, number representation, schema validation order or downstream consumer. A library's ability to read a file is not your application's acceptance contract.
Run the JSON acceptance fixture, retain the source payloads and terminal results, and require the intended consumer to preserve your declared contract before accepting an export.
Sources and method
Python JSON decoder documentation, retrieved October 1, 2026:
strict, object-pair hooks and number-decoding controls. The documentation surface and tested runtime are not claimed to be the same patch version.RFC 8259, objects and numbers: unique-name interoperability, number grammar and receiver range limitations.
Aident's synthetic native execution completed October 1, 2026, at 20:31:22 UTC: Python 3.13.14, 12 payloads, four policies, five additional assertions, successful terminal result and empty error/stderr. The exact executed fixture is included above.
About the author

Todd Mercer
Todd Mercer is a senior engineer with more than 10 years of experience working on social media platforms and large-scale ordering systems. He graduated from the University of Illinois Urbana-Champaign in computer science. At Aident, he covers technical issues, developer tools, and technology topics.



The one tool
for every tool
your agent needs.
Give any AI agent real capabilities in seconds. Connect 27,000+ tools once, skip the setup headache, and let your agents execute.


