feat: harden spec immutability#968
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces defensive copying and immutability for collection fields across several record classes and builders in the org.a2aproject.sdk.spec package using List.copyOf and Map.copyOf. The review feedback suggests further refining these changes by replacing @Nullable Boolean with a primitive boolean in MessageSendConfiguration and utilizing explicit canonical constructors in CancelTaskParams to avoid redundant null checks on record accessors when non-null default values are guaranteed.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
be8709f to
1151c80
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces defensive copying of collections and maps across various specification records (such as AgentCard, AgentSkill, Artifact, and others) to ensure immutability, along with comprehensive unit tests to verify this behavior. The review feedback correctly points out a critical issue: using Map.copyOf on arbitrary metadata or parameter maps will throw a NullPointerException if any values are null, which is common in JSON-based protocols. It is recommended to use Collections.unmodifiableMap(new HashMap<>(...)) instead to safely preserve immutability while supporting null values.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| public AgentCardSignature { | ||
| Assert.checkNotNullParam("protectedHeader", protectedHeader); | ||
| Assert.checkNotNullParam("signature", signature); | ||
| header = header == null ? null : Map.copyOf(header); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary header maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata/header maps. If a client sends a payload with a null value in the header, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| header = header == null ? null : Map.copyOf(header); | |
| header = header == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(header)); |
| */ | ||
| public AgentExtension { | ||
| Assert.checkNotNullParam("uri", uri); | ||
| params = params == null ? null : Map.copyOf(params); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary parameter maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in parameters. If a client sends a payload with a null value in the parameters, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| params = params == null ? null : Map.copyOf(params); | |
| params = params == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(params)); |
| throw new IllegalArgumentException("Parts cannot be empty"); | ||
| } | ||
| parts = List.copyOf(parts); | ||
| metadata = metadata == null ? null : Map.copyOf(metadata); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| metadata = metadata == null ? null : Map.copyOf(metadata); | |
| metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)); |
| */ | ||
| public CancelTaskParams { | ||
| Assert.checkNotNullParam("id", id); | ||
| metadata = Map.copyOf(metadata); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| metadata = Map.copyOf(metadata); | |
| metadata = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)); |
| */ | ||
| public MessageSendParams { | ||
| Assert.checkNotNullParam("message", message); | ||
| metadata = metadata == null ? null : Map.copyOf(metadata); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| metadata = metadata == null ? null : Map.copyOf(metadata); | |
| metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)); |
| Assert.checkNotNullParam("taskId", taskId); | ||
| Assert.checkNotNullParam("artifact", artifact); | ||
| Assert.checkNotNullParam("contextId", contextId); | ||
| metadata = metadata == null ? null : Map.copyOf(metadata); |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| metadata = metadata == null ? null : Map.copyOf(metadata); | |
| metadata = metadata == null ? null : java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)); |
| Assert.checkNotNullParam("taskId", taskId); | ||
| Assert.checkNotNullParam("status", status); | ||
| Assert.checkNotNullParam("contextId", contextId); | ||
| metadata = metadata != null ? Map.copyOf(metadata) : null; |
There was a problem hiding this comment.
Using Map.copyOf on arbitrary metadata maps (like Map<String, Object>) is risky because Map.copyOf throws a NullPointerException if the map contains any null values. In JSON-based protocols, null values are common and valid in metadata. If a client sends a payload with a null value in the metadata, deserialization will fail with a NullPointerException. Using Collections.unmodifiableMap(new HashMap<>(...)) is a safer alternative that preserves immutability while supporting null values.
| metadata = metadata != null ? Map.copyOf(metadata) : null; | |
| metadata = metadata != null ? java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata)) : null; |
|
i will fix it |
or no
what do you think? @kabir
|
|
@Sh1bari I see this in the javadoc for
However, my local Claude instance says that there are other occurrences of |
1151c80 to
231dfc5
Compare
|
@kabir done |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces defensive copying and immutability enhancements across various record classes in the SDK specification (such as AgentCard, AgentCapabilities, and CancelTaskParams) to prevent external mutation of internal collections and maps. It also adds a comprehensive suite of unit tests to verify these immutability behaviors. The review feedback highlights potential NullPointerException risks in CancelTaskParams when metadata is null, recommending graceful handling by defaulting to empty maps in both the compact constructor and the builder.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
@jmesnil Can you please take a look? |
| @@ -0,0 +1,109 @@ | |||
| package org.a2aproject.sdk.server.tasks; | |||
There was a problem hiding this comment.
is this test related to the spec hardening?
There was a problem hiding this comment.
No, this test was accidentally deleted in feat: Implement MainEventBus architecture for event queue processing (#611). I just rollback it

| defaultInputModes = List.copyOf(defaultInputModes); | ||
| defaultOutputModes = List.copyOf(defaultOutputModes); | ||
| skills = List.copyOf(skills); | ||
| securitySchemes = securitySchemes == null ? null : Map.copyOf(securitySchemes); |
There was a problem hiding this comment.
There are cases where you use nullability?: + Map.copyOf, others with Collections.unmodifiableMap(new HashMap<> with or without nullabillity?: and it's not clear why these choices were made.
The code would be more readable if these was using helper methods that would describe the hardening choices.
There was a problem hiding this comment.
is it? I was not asking to use Collections.unmodifiableMap instead of copyOf.
I was suggesting to use helper methods to give more helpful information on the initialization of the data structures of the spec (eg this field can be safely copied, this one requires to use an unmodifiable map because null entry and/or values are allowed).
There was a problem hiding this comment.
+1 to using helper methods.
FYI Map.copyOf() has some issues if any of the keys/values are null, so we were steered towards using Collections.unmodifiableMap()
There was a problem hiding this comment.
spec/src/main/java/org/a2aproject/sdk/util/CollectionCopies.java
Pushed a utility class with helper methods for the defensive-copy strategies. Could you please take a look?
I propose using this helper to make the intent explicit at call sites. If the approach looks good to you, I’ll update the usages in this PR to use it.
@jmesnil @kabir
231dfc5 to
19693d3
Compare
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.*; |
There was a problem hiding this comment.
star imports are not allowed
d478368 to
48d517f
Compare


Spec cleanup: immutable records and copy semantics
Build fails after
actions/checkoutwas bumped from v6 to v7 in PR #948:#948