Skip to content

fix: omit optional fields with default values from client JSON serialization - #774

Open
jmesnil wants to merge 1 commit into
a2aproject:mainfrom
jmesnil:770_omit_default_fields
Open

fix: omit optional fields with default values from client JSON serialization#774
jmesnil wants to merge 1 commit into
a2aproject:mainfrom
jmesnil:770_omit_default_fields

Conversation

@jmesnil

@jmesnil jmesnil commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

The JSON-RPC client serialization used alwaysPrintFieldsWithNoPresence() which caused proto3 implicit-presence fields (taskId, contextId, tenant, etc.) to be serialized as empty strings instead of being omitted.

Removed alwaysPrintFieldsWithNoPresence() from JsonFormat.printer() in JSONRPCUtils.toJsonRPCRequest() (client requests only), aligning with the REST client transport which already omitted it.

This fixes #770

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors JSON serialization in JSONRPCUtils.java by removing the alwaysPrintFieldsWithNoPresence() configuration, which results in more compact JSON output. Correspondingly, several test cases in JsonMessages.java were updated to remove empty metadata objects and default boolean fields. Feedback suggests optimizing performance by defining JsonFormat.Printer instances as static final constants to avoid redundant object creation during serialization.

Comment thread spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java Outdated
…ization

The JSON-RPC client serialization used alwaysPrintFieldsWithNoPresence()
which caused proto3 implicit-presence fields (taskId, contextId, tenant, etc.)
to be serialized as empty strings instead of being omitted.

Removed alwaysPrintFieldsWithNoPresence() from JsonFormat.printer() in
JSONRPCUtils.toJsonRPCRequest() (client requests only), aligning with the
REST client transport which already omitted it.

This fixes a2aproject#770
@jmesnil
jmesnil force-pushed the 770_omit_default_fields branch from c5c4c3b to 47dc31b Compare March 27, 2026 08:32
@chopmob-cloud

Copy link
Copy Markdown

Verified against current main: the defect this PR fixes is still live, and the diagnosis in #770 is correct.

Root cause confirmed. On main, spec-grpc/.../JSONRPCUtils.java still serialises client requests with alwaysPrintFieldsWithNoPresence():

  • toJsonRPCRequest(...) (client request path), line 580:
    JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace()
  • toJsonRPCResultResponse(...) (server response path), line 598: same call.

Because proto3 scalar fields use implicit presence, alwaysPrintFieldsWithNoPresence() forces every unset scalar into the JSON, which is exactly the "taskId":"" / "contextId":"" / "tenant":"" output captured in #770. That is a real interop hazard: on the wire, absent and empty are different assertions. A server that correctly distinguishes "no task id supplied" (start a new task, mint an id) from "task id is the empty string" will, on receiving "taskId":"", either reject or, as #770 shows against the .NET agent, echo an empty task.id that the a2a-java client then fails to deserialise. Omitting the field is the correct behaviour and matches the REST transport, which already omits it. So removing alwaysPrintFieldsWithNoPresence() from the client request printer is the right fix.

One scope note on the current diff: it changes only the request printer and leaves the response printer emitting empty scalars. That is a defensible split (this PR targets what the client sends, per the title), but the same absent-versus-empty hazard applies to toJsonRPCResultResponse on line 598, and a server emitting "id":"" in a result is the mirror image of the bug #770 hit. Worth a line in the PR on whether the response side is intentionally deferred.

Merge state: main has moved since this PR was opened (2026-03-27). The package was renamed from io.a2a.grpc.utils to org.a2aproject.sdk.grpc.utils and the printer is now passed into a ProtoJsonUtils.toJson(printer, payload) wrapper rather than being called inline with .print(payload). GitHub reports the PR as mergeable: false / mergeable_state: dirty for that reason. The change is still correct and small; it just needs a rebase onto the new path and the new ProtoJsonUtils.toJson(...) call shape (drop .alwaysPrintFieldsWithNoPresence() from the printer constructed for the request path). No behavioural rework needed, only relocation.

For grounding, this absent-is-not-empty rule is one we hold on our own signing and canonicalisation paths, so the concern is not theoretical for us: optional fields are omitted from the preimage rather than emitted empty, because an empty placeholder changes the bytes that get signed and verified.

Thanks for chasing this down to the printer; the diagnosis is right and the fix is the minimal correct one once rebased.

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

2 participants