Skip to content

fix: omit unset proto3 fields from JSON-RPC client requests - #1286

Open
WMK666666 wants to merge 1 commit into
a2aproject:mainfrom
WMK666666:issue-770
Open

WMK666666 wants to merge 1 commit into
a2aproject:mainfrom
WMK666666:issue-770

Conversation

@WMK666666

Copy link
Copy Markdown
Contributor

Problem

JSONRPCUtils.toJsonRPCRequest serialised client requests with alwaysPrintFieldsWithNoPresence(). proto3 scalar fields have implicit presence, so every field the caller never set was forced onto the wire as "", and repeated fields as []:

"params": {
  "tenant": "",
  "message": {
    "messageId": "...", "taskId": "", "contextId": "", "role": "ROLE_USER",
    "parts": [{ "text": "...", "filename": "", "mediaType": "" }],
    "extensions": [], "referenceTaskIds": []
  }
}

On the wire those are two different assertions — "I am not supplying a task id" versus "the task id is the empty string" — and they cannot be told apart again once the request is parsed. That is what #770 reports: the .NET agent was told a task id was present but empty, replied with "id": "", and the a2a-java client then failed to deserialise the empty id.

Fix

Serialise requests without that flag and keep it for responses:

before after
request (toJsonRPCRequest) alwaysPrintFieldsWithNoPresence() plain printer
response (toJsonRPCResultResponse) alwaysPrintFieldsWithNoPresence() unchanged

This is not a new convention, it aligns the JSON-RPC client with the sibling implementations that already do it:

  • client/transport/rest/.../RestTransport.java posts with a plain JsonFormat.printer() on both of its paths — the REST client already omits unset fields.
  • transport/rest/.../RestHandler.java keeps alwaysPrintFieldsWithNoPresence() when writing responses — the REST server also keeps it.

So only the JSON-RPC request side was the odd one out; the response side stays as-is on purpose.

Both printers are now static final fields instead of being rebuilt per call, and both still go through ProtoJsonUtils.toJson, so the HTML-unescaping fix from #892 is preserved.

Tests

Updating the fixtures alone would not catch a regression here, because the fixtures are just mockserver expectations. Two unit tests pin the behaviour directly:

  • testToJsonRPCRequest_omitsFieldsWithoutPresence — asserts tenant, taskId, contextId, extensions, referenceTaskIds, filename and mediaType are all absent from the serialised request, then round-trips it through parseRequestBody.
  • testToJsonRPCResultResponse_stillPrintsFieldsWithoutPresence — pins the other half of the split so the two printers are not collapsed into one later.

Fixture side: the JsonMessages request fixtures no longer carry "returnImmediately":false or an empty "metadata":{} object — with the flag gone the client does not send them any more, and the stubs match with MatchType.ONLY_MATCHING_FIELDS. The two ListTasksParams assertions in JSONRPCTransportTest now assert the tenant key is absent instead of asserting it is "". Response fixtures are untouched.

Verification

mvn -pl :a2a-java-sdk-spec-grpc,:a2a-java-sdk-client-transport-jsonrpc -am test
  -> 658 tests, 0 failures

mvn -pl server-common,client/base,compat-0.3/client/transport/jsonrpc,compat-0.3/spec-grpc -am test
  -> 1567 tests, 0 failures over 16 modules

Reverse check: pointing toJsonRPCRequest back at the response printer makes testToJsonRPCRequest_omitsFieldsWithoutPresence fail with exactly the payload from #770 —

{"jsonrpc":"2.0","id":"test-id","method":"SendMessage","params":{"tenant":"","message":{"messageId":"message-1","contextId":"","taskId":"","role":"ROLE_USER","parts":[{"text":"hello","metadata":{},"filename":"","mediaType":""}],"metadata":{},"extensions":[],"referenceTaskIds":[]},"metadata":{}}}

— and nothing else in JSONRPCUtilsTest fails, i.e. the test really is guarding this flag and not passing for some unrelated reason.

Relation to #774

This is the same fix as #774 (thanks @jmesnil), which was closed for inactivity rather than on merit. It cannot be applied as-is: the packages moved from io.a2a to org.a2aproject, and both call sites now go through ProtoJsonUtils.toJson instead of calling Printer.print directly — that indirection is the #892 HTML-escaping fix, which #774 predates. So this is #774 rebased onto e5483dd4f3, plus the static printers from the earlier review and the two tests above.

Happy to close this if you would rather reopen #774 and drive it from there.

Fixes #770

toJsonRPCRequest() serialised with alwaysPrintFieldsWithNoPresence(), so
proto3 scalars the caller never set went out as "" (tenant, taskId,
contextId, filename, mediaType) and repeated fields as []. A peer cannot
tell "not supplied" from "supplied but empty" any more: in a2aproject#770 the .NET
agent was told the task id was present but empty, answered with an empty
task.id, and the client then failed to deserialise it.

Requests now use a plain printer, which is what the REST client already
does. Responses keep alwaysPrintFieldsWithNoPresence() to match the REST
server handler, so clients still see the full payload shape. Both
printers are static fields now instead of being rebuilt per call, and
both still go through ProtoJsonUtils.toJson so the HTML-unescaping from
a2aproject#892 is preserved.

Adds two unit tests: one asserting the request omits those fields, one
pinning that the response printer still prints them.

Fixes a2aproject#770

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: a2a-java client sends message with empty task ID and context ID

1 participant