Why The Same JSON Request Can Mean Two Different Things
I ran the same JSON through JavaScript and Go. One program read preview, the other read commit, and neither reported an error. How case-insensitive field matching, duplicate keys and re-serialization let a gateway and a backend disagree about what a request means.

I ran the same JSON through JavaScript and Go. One program read preview. The other read commit. Both accepted the input without errors.
Consider this JSON:
{
"mode": "preview",
"Mode": "commit"
}What should a program do with it?
At first glance, there's nothing particularly interesting here. It's a JSON object containing two properties.
JavaScript treats them as separate properties:
const request = JSON.parse(
'{"mode":"preview","Mode":"commit"}'
);
console.log(request.mode); // "preview"
console.log(request.Mode); // "commit"Now consider the same request in Go:
type Request struct {
Mode string `json:"mode"`
}
var request Request
json.Unmarshal(
[]byte(`{"mode":"preview","Mode":"commit"}`),
&request,
)
fmt.Println(request.Mode) // "commit"Same bytes. Different result.
JavaScript preserves two distinct properties. Go's original encoding/json decoder maps both names to the same struct field.
Because the later value wins for this string field, Go ends up with "commit".
Both programs successfully parsed the input.
Neither reported an error.
This is where things get interesting.
Imagine the JavaScript code belongs to an API gateway responsible for validating requests. It checks request.mode, sees "preview", and allows the request through.
The Go backend receives the same JSON, decodes it into a struct, and sees "commit".
The gateway and the backend have different interpretations of what the client requested.
For that to become an actual vulnerability, the application would need to authorize one interpretation and execute the other without a suitable downstream check. But the underlying disagreement is real.
I wanted to understand how easily that disagreement could occur, and why it matters for systems that rely on JSON for security-sensitive decisions.
JSON Isn't Quite as Simple as It Looks
JSON has become one of the default formats for communication between software systems.
APIs use it. Applications store configuration in it. Webhooks deliver it. AI systems use it to represent tool calls and structured responses.
It seems straightforward.
There are objects, arrays, strings, numbers, booleans, and null.
The specification is relatively small, and almost every programming language has a JSON parser.
That familiarity makes it easy to assume that a JSON document has one meaning, regardless of which application reads it.
Unfortunately, interoperability has some sharp edges.
RFC 8259 recommends that names within a JSON object be unique.
It also explains that behavior becomes unpredictable when names are duplicated.
Some parsers retain the last value. Others may reject the object or expose multiple occurrences.
Consider:
{
"approved": false,
"approved": true
}Which value should the application use?
The JSON specification doesn't require every implementation to resolve this situation in the same way.
That creates an obvious problem when one component makes a security decision and another component interprets the same input differently.
But our original example has another complication.
The keys aren't identical.
{
"mode": "preview",
"Mode": "commit"
}They're distinct strings with different capitalization.
JavaScript treats them as separate properties. Python does too.
So why does Go collapse them?
Go's Case-Insensitive Field Matching
Go's original encoding/json API is designed to make decoding JSON into structs convenient.
When it encounters an object member, it tries to match the member name to the destination struct's field name or JSON tag.
It prefers an exact match.
But it also accepts a case-insensitive match.
That behavior is documented in the Go JSON package.
Consider:
type Request struct {
Mode string `json:"mode"`
}The key "mode" matches the struct tag exactly.
The key "Mode" also matches because the decoder accepts case-insensitive field-name matches.
When both appear in the input, the second value overwrites the first for this string field.
The order matters.
I tested both arrangements:
{"mode":"preview","Mode":"commit"}Result:
Mode = "commit"Now reverse the order:
{"Mode":"commit","mode":"preview"}Result:
Mode = "preview"Nothing else changed.
Both values are valid JSON strings. Both names match the destination struct field under the decoder's rules.
The last one wins.
This is convenient when processing ordinary data from clients that aren't perfectly consistent about capitalization.
It's much less convenient when field names have security-sensitive meanings.
And the issue becomes especially interesting when the application serializes the struct back to JSON.
The Round Trip Changes the Message
Take the original input:
{
"mode": "preview",
"Mode": "commit"
}Decode it into our Go struct.
Then serialize the struct back into JSON:
normalized, _ := json.Marshal(request)
fmt.Println(string(normalized))The result is:
{
"mode": "commit"
}Look carefully at what happened.
The original document contained two distinct keys.
The resulting document contains one.
The value associated with the lowercase mode key changed from "preview" to "commit".
The transformation is perfectly consistent with the Go decoder's behavior.
But it means that a component attempting to normalize or reconstruct a request can change its effective meaning.
Now consider a pipeline:
Client
|
v
JavaScript Gateway
mode = "preview"
|
v
Go Service
Mode = "commit"
|
v
Re-serialized Request
mode = "commit"If the gateway makes an authorization decision using one interpretation, the downstream system may act on another.
The problem arises because the components disagree about the representation of the same request.
And that leads to another surprising result.
DisallowUnknownFields() Doesn't Fix This
Go provides a method called DisallowUnknownFields().
Its purpose is to make JSON decoding stricter by rejecting object fields that don't correspond to the destination struct.
That sounds like exactly what we'd want.
Let's try it.
type Request struct {
Mode string `json:"mode"`
}
raw := `{"mode":"preview","Mode":"commit"}`
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.DisallowUnknownFields()
var request Request
err := decoder.Decode(&request)
fmt.Println(request.Mode) // "commit"
fmt.Println(err) // <nil>The decoder accepts the input.
No error.
The reason is straightforward once you understand how matching works.
DisallowUnknownFields() rejects fields that don't match the destination struct.
But "Mode" does match.
It's a case-insensitive match, and the original decoder considers that sufficient.
I ran four variations to check the behavior.
| JSON input | Result with DisallowUnknownFields() |
|---|---|
{"mode":"preview","Mode":"commit"} |
Accepted, value commit |
{"mode":"preview","mode":"commit"} |
Accepted, value commit |
{"mode":"preview","mode":"commit"} |
Accepted, value commit |
{"mode":"preview","unknown":"commit"} |
Rejected |
The third example is worth examining.
The sequence d represents the letter d.
After JSON string decoding, the property name mode becomes mode.
The parser therefore encounters the same decoded name twice.
And once again, the last value wins.
This is a reminder that strictness isn't a single switch.
Rejecting unknown fields is useful. Rejecting duplicate fields is useful. Enforcing exact member-name matching is useful.
They solve related but different problems.
For a protocol whose security model depends on exact field names, a decoder that accepts multiple spellings of the same logical field can introduce ambiguity.
That ambiguity needs to be resolved before the application makes authorization decisions.
This Has Already Caused a Real Security Problem
In May 2026, a GitHub security advisory described a protocol parser differential vulnerability in Envoy AI Gateway.
The affected versions were earlier than 0.6.0. The advisory identifies version 0.6.0 as patched.
The issue involved Model Context Protocol (MCP) messages and the way different components interpreted JSON-RPC fields.
JSON-RPC 2.0 requires member names used for matching to be case-sensitive.
That matters because JSON-RPC messages often contain fields that determine what operation a system performs.
For example, MCP tool calls contain parameters identifying which tool to invoke.
The advisory described a situation where one component could interpret a properly capitalized field while another decoder accepted a differently capitalized variant.
When the gateway reconstructed the message, its parsing and serialization behavior could transform the requested operation.
The resulting message could carry a different tool name than the one recognized by an earlier security check.
That's a particularly serious kind of disagreement.
A gateway might correctly enforce a rule based on the message it understands, while a later component executes a different interpretation.
The advisory explains how this could affect authorization boundaries in multi-layered architectures.
Our local experiment doesn't reproduce that complete vulnerability. It demonstrates the same underlying class of parser behavior using harmless preview and commit values.
The real case involved specific gateway and SDK implementations, routing logic, and protocol expectations.
The important lesson is that this class of problem has practical security consequences.
When software components disagree about the meaning of a request, a security check performed by one component may not protect the operation performed by another.
The Problem Exists Beyond Go
It's tempting to treat this as a Go-specific issue.
But parser disagreement is a broader interoperability problem.
Consider JSON numbers.
I also tested this input:
{
"id": 9007199254740993
}In Python:
import json
value = json.loads(
'{"id":9007199254740993}'
)
print(value["id"])
# 9007199254740993In JavaScript:
const value = JSON.parse(
'{"id":9007199254740993}'
);
console.log(value.id);
// 9007199254740992Python's integer representation preserves the original value.
JavaScript's ordinary Number representation cannot represent that integer exactly.
The result differs by one.
This is well-known behavior for integers beyond JavaScript's safe-integer range.
But consider what it means when numbers represent database identifiers, account references, or other values that need to remain exact across services.
One component might interpret a number precisely while another loses information.
If an application uses those values for security-sensitive comparisons, the mismatch deserves attention.
The same general principle applies to duplicate object members, Unicode normalization, number formatting, and other differences in parsing and serialization.
The danger appears when the disagreement crosses a trust boundary.
The Security Boundary Is the Important Part
Imagine a system with three components.
The first accepts an external request.
The second validates that request.
The third performs an operation.
The system is safe only if the operation performed is consistent with what the security checks authorized.
That sounds obvious.
But distributed systems make it surprisingly easy to violate this principle.
An API gateway may be written in one language. Its authorization middleware may use another library. A backend may deserialize requests into strongly typed objects.
A queue might serialize the data again before another service reads it.
Each conversion creates an opportunity for the representation to change.
Usually, these transformations are harmless.
Sometimes they are intentional.
But for security-sensitive fields, the application must ensure that the meaning remains consistent.
Consider a request containing an operation name.
If the authorization layer sees "preview" while the executor sees "commit", the authorization check has lost its connection to the operation being performed.
The important security property is that the authorized action and executed action must be identical under the application's protocol rules.
Everything else follows from that.
How to Avoid Parser Disagreement
There are several useful defenses, and they reinforce one another.
Reject ambiguous input at the boundary
For security-sensitive protocols, duplicate JSON member names should generally be rejected.
It's important to do this before parsing has discarded information about repeated fields.
Once a parser converts a JSON object into an ordinary map, earlier duplicate members may already be lost.
You can't reliably detect the original duplicates by inspecting the resulting map.
A strict parser needs to recognize duplicate member occurrences while reading the original input, including equivalent names expressed using JSON escape sequences.
Enforce the protocol's exact naming rules
JSON-RPC explicitly requires case-sensitive member-name matching.
A decoder that accepts "Name" when the protocol expects "name" is being more permissive than the protocol.
That permissiveness may be convenient for ordinary application data, but it can be dangerous for protocol control fields.
Validate names according to the protocol specification, not merely according to the convenience rules of a language's default decoder.
Keep authorization close to execution
An authorization check should validate the same logical operation that the application will perform.
If a gateway authorizes a request and a downstream component later reinterprets or reconstructs it, make sure the transformation cannot change the authorized fields.
When possible, carry a validated, unambiguous representation forward rather than repeatedly interpreting untrusted input.
The final executor should enforce the security properties it depends on.
Be careful with normalization
Normalization can make requests easier to process, but it can also hide ambiguity.
Our Go example is a good illustration.
The original input had two distinct property names.
After decoding and encoding, the output had one.
If the application intended to preserve the original request's meaning, that transformation was significant.
Normalization at a security boundary should follow explicit rules, reject ambiguous inputs, and be tested for semantic changes.
Test the boundaries between languages
Unit tests for individual services are valuable.
But this class of problem often lives between services.
A useful regression test sends the same input through the actual validation and execution paths, then checks that both agree on the operation and its parameters.
Include duplicate fields, differently capitalized names, escaped property names, and large numbers.
The goal is to identify interpretation differences before an attacker does.
Go Is Already Improving This
There's an important development worth mentioning.
Go 1.27 introduced encoding/json/v2, a major revision of the original JSON API.
According to the official migration guide, the new API chooses stricter defaults.
Among the changes:
- Duplicate JSON member names are rejected by default.
- Struct field-name matching is case-sensitive by default.
- Invalid UTF-8 in JSON strings is rejected rather than silently replaced.
These changes address several longstanding sources of inconsistent behavior.
The original encoding/json API remains supported for compatibility, with its legacy semantics preserved.
That distinction matters. The local demonstration in this article used Go 1.23.2 and the original encoding/json API. It should not be generalized to every Go JSON decoder or to the stricter v2 API.
If you're working with security-sensitive JSON in Go, the newer API is worth evaluating.
For existing applications, review exactly which decoder semantics are being used and test the behavior at protocol boundaries.
A library upgrade doesn't automatically fix an architecture where authorization and execution interpret requests differently.
The Lesson
The most interesting part of this experiment is how little it took to produce a disagreement.
No malformed JSON syntax.
No concurrency bug.
No complex parsing trick.
Just two property names with different capitalization.
JavaScript preserved them as distinct properties.
Go's original JSON decoder mapped them to one struct field, and the last value won.
DisallowUnknownFields() accepted the input.
When Go serialized the struct again, the original request had effectively changed.
That behavior is documented, and it can be perfectly reasonable in applications where permissive decoding is desirable.
But permissive decoding becomes a problem when different components use different rules to interpret security-sensitive requests.
The 2026 Envoy AI Gateway advisory showed that this is more than an academic concern.
Parsing and serialization decisions can affect authorization boundaries.
We often think about API security in terms of authentication, permissions, input validation, and encryption.
Those things matter.
But they depend on a more fundamental assumption: every component involved in a security decision agrees about what the request means.
If that assumption fails, even a correctly implemented authorization check can become ineffective.
The next time a request passes through a gateway, middleware, serializer, and backend, consider what happens to its meaning along the way.
The bytes can remain the same while the interpretation changes. And a round trip through a serializer can turn that disagreement into a different request entirely.
Methodology and references
The local reproductions were run on Linux using Go 1.23.2, Node.js 22.16.0, and Python 3.13.5.
The tests compared case-variant JSON member names, duplicate names, Unicode-escaped member names, field-order changes, and large integer parsing.
They also tested Go's original encoding/json.Decoder.DisallowUnknownFields() behavior.
The examples demonstrate parser semantics. They do not constitute an exploit against a deployed application or a reproduction of the complete Envoy AI Gateway vulnerability.
Primary references:



