Key takeaways
- RFC 8259 recommends unique object names but does not require every parser to reject duplicates; it calls non-unique behavior unpredictable across implementations.
- In all four duplicate fixtures, the tested Node, Python and Ruby parsers agreed on the parsed values and retained the last member.
- A first-match preview displayed /workspace/readme.md, while JSON.parse produced /workspace/secrets.env for execution. Approval is unsafe when it and execution do not consume the same canonical object.
- A JSON.parse reviver was too late to detect the duplicate path because it visited the collapsed key once. Reject duplicates while consuming raw member events, before object construction.
- The bounded fix is not “trust the last value.” Parse once with duplicate rejection, validate the resulting typed object, render approval from that object, bind approval to its canonical digest, and execute that exact immutable value.
The decision is reject duplicates before an AI tool call becomes an object
Reject any model-produced tool-call object containing a repeated decoded member name. Then schema-validate one parsed value, render the approval prompt from that value, bind the approval to its canonical digest and execute the same immutable value. Do not let a regex, log formatter, streaming preview or second parser reconstruct what the user approved.
This is a host-side agent control. It does not depend on whether the model intended the duplicate, whether structured generation usually avoids it, or whether the final member looks reasonable. Ambiguous action parameters should fail closed before authority is granted.
The sharp question is what the reviewer approved
A tool schema can require one path property, yet raw JSON can physically contain that name twice. If a preview selects the first occurrence and the executor’s parser retains the last, both components can claim they handled path while referring to different targets.
For an AI agent, that is not merely a JSON style problem. The string sits on the boundary between probabilistic model output and a consequential operation: reading a secret, deleting a file, sending a message, purchasing an item or changing infrastructure.
The standard makes uniqueness an interoperability condition
RFC 8259 section 4 says names within an object SHOULD be unique. It says unique names make the object interoperable because receiving software agrees on the mappings. When names are not unique, it calls receiver behavior unpredictable and notes that implementations may retain the last pair, reject the text or expose every pair.
“SHOULD” is not the same as a wire-level guarantee that every conforming parser rejects ambiguity. An agent host that requires one approved action therefore needs a stricter local contract than generic JSON acceptance.
The chronology runs from JSON interoperability to agent authority
JSON’s current IETF specification was published as RFC 8259 in December 2017. Python’s standard-library documentation explicitly records a permissive default: repeated names are accepted and only the last value is used. JavaScript and Ruby are common implementation languages in agent stacks, so we tested their installed parsers rather than inferring parity from the RFC.
By August 2026, OpenAI’s agent-building guidance recommended structured outputs and tool approvals as layers against unsafe data flow. On September 12, we tested the seam between those layers: whether approval and execution still agree when a model response is syntactically accepted but contains duplicate members.
Five fixtures isolate the parser boundary
The harness uses one valid control plus four duplicate cases. They repeat the top-level tool name, a nested path, a nested approved flag and path written once literally and once as pa\u0074h. JSON decoding makes the escaped spelling the same member name.
These are contract fixtures, not sampled model generations. Their purpose is to make one ambiguity deterministic and auditable. The experiment does not estimate how often any model emits duplicate names.
Node, Python and Ruby all retained the final member
Node v24.10.0 JSON.parse, Python 3.9.6 json.load and Ruby 2.6.10 JSON.parse produced structurally equal values for all five fixtures. In each of the four duplicate cases, the later value survived object construction.
That agreement is a local observation, not a portable rule. It is consistent with the RFC’s note that many implementations report only the last pair and with Python’s documented behavior. Another parser, version or streaming library can reject or expose duplicates instead.
The approval preview and executor disagreed on the path
The deliberately unsafe preview searched the raw tool call for the first path pair. It displayed /workspace/readme.md. The executor called JSON.parse on the same bytes and received /workspace/secrets.env as arguments.path.
No file was read and no external action was executed; the harness compares values only. Still, the contract failure is complete: a reviewer could approve one visible target while the tool receives another. The artifact records both strings under approvalExecutionSplit.
A reviver cannot recover information already collapsed
The harness passed a reviver to JSON.parse and recorded visited keys. For the duplicate nested-path fixture, the reviver observed path once. By the time reviver traversal began, JSON.parse had already constructed an object with one surviving property.
Therefore a reviver that rejects repeated visits cannot enforce duplicate uniqueness in this path. Detection must happen while reading raw member events, or through a parser mode that preserves every pair before object construction.
Escaped names defeat byte-level duplicate checks
One fixture contains path and pa\u0074h. They are different byte sequences but decode to the same JavaScript string. The event-preserving scanner flagged $.arguments.path as a duplicate after decoding the member names.
A preflight that compares only raw source slices can miss this equivalence. Duplicate policy belongs at the JSON token layer: decode valid string escapes, track names per object scope and reject the second occurrence before building a map.
Schema validation is necessary but may arrive too late
A schema validator receiving an ordinary object sees one path, because duplicate history has already been discarded. It can still reject the surviving value for type, format, enum or authorization reasons, but it cannot prove that approval and execution saw the same raw intent.
Use the schema after duplicate-aware parsing, not instead of it. Validate types and business rules on the sole canonical object, then pass that exact value forward. Do not parse model text independently in the UI, policy service and tool worker.
One parse closes the representation gap
The safe sequence is: frame one complete JSON value; reject malformed input, duplicate decoded names and trailing data; construct the object once; validate it against the pinned tool schema; apply deterministic authorization; canonicalize the approved fields; and compute a digest that includes tool name, arguments, principal, destination and policy version.
Render the approval interface from that validated object. On approval, send the immutable object and digest to execution. The worker verifies the digest and authorization immediately before acting. Any mutation, retry substitution or policy-version change requires a new approval.
Streaming needs a commit point, not a live approval
A streaming model can emit an early path and later emit another. Do not make an approval actionable while the object is incomplete, and do not treat a partial preview as the committed request. Buffer within explicit size and time limits until one complete value is accepted.
If low-latency preview is useful, label it uncommitted. Replace it only with fields from the final duplicate-checked object. Approval buttons should remain disabled until framing, parsing, schema, policy and canonicalization all pass.
Retries can recreate the same bug across messages
Even when each JSON object has unique keys, a retry can change arguments after approval. Bind approval to one attempt and one canonical digest; do not approve a tool name broadly and let a later model turn supply new parameters.
Record the raw response hash, parser and version, schema hash, canonical object digest, policy result, approval identity, execution receipt and retry lineage. Redact sensitive values where required, but preserve enough evidence to prove that the acted-on parameters matched the reviewed parameters.
The first harness failure exposed a comparison mistake
The initial run failed on the unique control because Python serialized object keys in sorted order while Node preserved insertion order. Comparing JSON strings therefore reported a false disagreement even though the parsed mappings were equal.
We changed the assertion to parse each runtime’s output and compare values structurally. This failure matters operationally: canonical digest algorithms need an explicit member-order rule. Ordinary serializer output from different runtimes is not automatically a shared canonical form.
Predeclare the deployment gate
A tool path passes only when duplicate names are rejected at every object depth after escape decoding; malformed and trailing input fail closed; one parsed value drives schema, authorization, approval and execution; canonical digests match across services; and retries cannot substitute parameters. Include benign controls so strictness does not break valid tool calls.
Test every production parser, language boundary, queue serializer and approval renderer. Include top-level, nested, escaped, array-contained and deeply nested duplicates plus size, depth and timeout limits. Treat any approved-versus-executed mismatch as non-compensable.
What would change this conclusion
A provider guarantee backed by wire-level evidence that duplicate names cannot be emitted would reduce exposure at that ingress, but the host should still defend alternate models, replayed traces, compromised intermediaries and hand-built requests. A protocol that transports typed values without reparsing JSON text could remove this particular seam.
The measured claim remains narrow: five fixtures, three installed runtimes and one unsafe preview. It proves a reproducible representation split, not exploitation, prevalence or vendor vulnerability. The production conclusion changes only when approval and execution are demonstrably bound to the same duplicate-free canonical value.
Copy-ready AI tool-call parsing and approval record
Complete one record per model ingress, parser path and consequential tool family. Attach fixture-level traces rather than a generic “uses JSON schema” assertion.
Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].
Agent, users, model/provider, tool family, data classes, action consequences, accountable owner and evidence expiry.
Framing, encoding, maximum bytes and depth, streaming commit point, malformed-input policy and trailing-data policy.
Parser and version, decoded-name detection at every object depth, escaped-name cases, rejection error and tests.
Tool schema and hash, required fields, additional-property policy, formats, semantic constraints and validator version.
Principal, tenant, resource, destination, policy version, deny behavior, least privilege and just-in-time recheck.
Fields rendered, canonicalization algorithm, object digest, expiry, approver identity and material-change rule.
Exact immutable object delivered to the worker, digest verification, execution receipt and side-effect idempotency key.
Attempt lineage, parameter substitution rule, parser/model fallback, reapproval condition and duplicate execution prevention.
Raw-response hash, fixture suite, cross-runtime results, valid controls, policy traces, approval screenshot and execution log.
Mandatory pass thresholds, non-compensable mismatch, canary scope, disable switch, rollback owner and incident drill.
Primary sources
Browse the publication-wide evidence index →
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange FormatRFC Editor / IETF · Reviewed: Section 4 Objects, including member-name uniqueness and interoperability behavior · Retrieved · Supports: RFC 8259 says object names SHOULD be unique, objects with unique names are interoperable, and behavior is unpredictable when names are not unique; it notes that many implementations report only the last pair while others error or preserve duplicates.
- json — JSON encoder and decoder: Standard Compliance and InteroperabilityPython Software Foundation · Reviewed: Repeated Names Within an Object and the default decoder’s documented behavior · Retrieved · Supports: Python documents that its json module accepts repeated names by default and uses only the value of the last name-value pair. This independently documents one runtime behavior; it does not define a safe agent protocol.
- JSON.parse()MDN Web Docs · Reviewed: Syntax, return value, reviver traversal and examples for parsing JSON text · Retrieved · Supports: MDN documents JSON.parse conversion and post-parse reviver traversal. The local harness, rather than MDN, establishes that the tested reviver received one decoded path key after duplicate collapse.
- Safety in building agentsOpenAI Developer Documentation · Reviewed: Structured outputs, tool approvals, untrusted data flow, guardrails and evaluation guidance · Retrieved · Supports: OpenAI’s vendor-authored guidance recommends structured outputs and tool approvals among layered agent controls. It does not specify duplicate-member handling and is not evidence that those controls are safe when approval and execution consume different representations.
Limitations
This is a deterministic contract investigation, not a model benchmark, red-team exercise, exploit demonstration or prevalence study. It used five hand-built JSON fixtures and tested Node v24.10.0 on Darwin arm64, Python 3.9.6 and Ruby 2.6.10; it did not test browser engines, Java, Go, Rust, streaming libraries, provider SDKs, JSON Schema implementations, gateways or production approval UIs. The unsafe preview was deliberately implemented with a first-match regular expression to expose a representation split; no production product is claimed to use that pattern. No model generated the fixtures, no external tool executed and no sensitive file was accessed. Three source links returned HTTP 200 during research; the OpenAI page was not freshly retrievable during this run, so only the publication’s documented 2026-08-09 review is reused. The findings do not establish a vulnerability in any named provider or agent framework.
Disclosures
AccessAllGPT designed and ran the local fixtures and received no vendor API credits, private data, advance access or compensation. No cited organization sponsored, reviewed or endorsed the harness or article. OpenAI documentation appears only as labeled vendor guidance; the experiment did not call an OpenAI model or API. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI, the IETF, Python Software Foundation, Mozilla or Ruby maintainers. Publication-wide relationships are listed on the disclosures page.
Further AccessAllGPT guidance
- Prompt Injection: Set the Deployment Gates Before Your LLM Can Act
- Human-in-the-Loop AI: Put Approval at the Consequence Boundary
- Before You Give a Coding Agent Repository Access
- Design an Agent Benchmark That Predicts Production
- LLM Observability: Build the Evidence Layer Before You Buy the Dashboard
- The AI Tooling Procurement Scorecard
- AccessAllGPT Research methodology
- Publication disclosures
Continue the research
Get evidence-led updates for teams making production AI decisions.