Python JSON Acceptance Test: Duplicate Keys Survive strict=True

Python JSON Acceptance Test: Duplicate Keys Survive strict=True

Todd Mercer

Apricot and lavender folded crescents remain separately visible against an ultramarine plane, representing duplicate names detected before information is lost.

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

id and escaped \u0069d in one object

Accept

Accept

Reject

Reject

id in two separate objects

Accept

Accept

Accept

Accept

Escaped newline in a string

Accept

Accept

Accept

Accept

Raw newline inside a string

Reject

Reject

Reject

Reject

NaN, Infinity or -Infinity

Accept

Accept

Accept

Reject

Numeric literal 1e400

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:

{ "id": "007", "id": "008" }

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.

import json
import math
import platform


def unique_object(pairs):
    result = {}
    for name, value in pairs:
        if name in result:
            raise ValueError("duplicate object name: " + repr(name))
        result[name] = value
    return result


def reject_constant(token):
    raise ValueError("nonfinite token: " + token)


def finite_floats(value):
    if isinstance(value, float) and not math.isfinite(value):
        raise ValueError("nonfinite decoded float")
    if isinstance(value, dict):
        for child in value.values():
            finite_floats(child)
    elif isinstance(value, list):
        for child in value:
            finite_floats(child)
    return value


def consumer_loads(text):
    return finite_floats(json.loads(
        text, object_pairs_hook=unique_object,
        parse_constant=reject_constant, strict=True
    ))


fixtures = [
    ("unique", '{"id":"007","amount":1.5}', [True, True, True, True]),
    ("duplicate", '{"id":"007","id":"008"}', [True, True, False, False]),
    ("nested", '{"row":{"id":1,"id":2}}', [True, True, False, False]),
    ("escaped_name", r'{"id":1,"\u0069d":2}', [True, True, False, False]),
    ("separate_objects", '[{"id":1},{"id":2}]', [True, True, True, True]),
    ("escaped_newline", r'{"note":"line\nnext"}', [True, True, True, True]),
    ("raw_newline", '{"note":"line\nnext"}', [False, False, False, False]),
    ("nan", '{"value":NaN}', [True, True, True, False]),
    ("infinity", '{"value":Infinity}', [True, True, True, False]),
    ("negative_infinity", '{"value":-Infinity}', [True, True, True, False]),
    ("overflow", '{"value":1e400}', [True, True, True, False]),
    ("trailing_document", '{"id":1} {"id":2}', [False, False, False, False]),
]
policies = [
    ("default", json.loads),
    ("strict_true", lambda text: json.loads(text, strict=True)),
    ("unique_names", lambda text: json.loads(text, object_pairs_hook=unique_object)),
    ("consumer_policy", consumer_loads),
]
results = []
for name, text, expected in fixtures:
    accepted = []
    errors = []
    for policy, load in policies:
        try:
            load(text)
            accepted.append(True)
            errors.append(None)
        except ValueError as error:
            accepted.append(False)
            errors.append(str(error))
    assert accepted == expected, (name, accepted, expected)
    results.append({"fixture": name, "accepted": accepted, "errors": errors})

assert json.loads(fixtures[1][1]) == {"id": "008"}
assert json.loads(fixtures[3][1]) == {"id": 2}
assert math.isinf(json.loads(fixtures[10][1], parse_constant=reject_constant)["value"])
assert json.loads(fixtures[6][1], strict=False)["note"] == "line\nnext"
assert consumer_loads("42") == 42  # Root-object shape is a separate policy.
print(json.dumps({
    "python": platform.python_version(),
    "policies": [name for name, _ in policies],
    "results": results,
    "all_assertions_passed": True,
}, allow_nan=False))

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:

Follow https://aident.ai/SETUP.md

Then ask:

Discover Code Interpreter Tools' Execute Code Action in Aident Loadout.
Inspect its current schema and preflight the complete acceptance fixture
from this article. Execute the unchanged code only after reviewing the
current cost and approval requirements. Return the actual Python version,
all 12 result rows, the assertion outcome and any terminal error or stderr.
Do not use customer payloads or treat accepted execution as a finished run

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

Illustrated editorial avatar of Todd Mercer

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.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

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.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.