Summary
Value.wrap() in the generated gRPC protobuf bindings (dist/chunk-*.js, compiled from src/grpc/pb/google/protobuf/struct.ts) sets the nullValue oneof case to the literal JS null instead of the NullValue.NULL_VALUE enum value (0). @bufbuild/protobuf's wire encoder strictly validates int32 fields and throws when it receives null, so any Task/Message (or other Struct-typed) field containing a JSON null value crashes gRPC serialization with Error: invalid int32: object.
The A2A spec defines metadata as "a flexible key-value map ... values can be any valid value that can be represented in JSON" — null is valid JSON, so this isn't a caller error, it's a bug in the wire encoding of the well-known Struct/Value types.
This is a known class of pitfall in ts-proto-generated code for google.protobuf.NullValue (see stephenh/ts-proto#458) — the fix is to map null to 0 (not pass null straight through) before writing the int32 field.
Reproduction
import { A2AService } from '@a2a-js/sdk/server/grpc'
const fakeTask = {
id: 'task-1',
contextId: 'ctx-1',
status: { state: 1, timestamp: new Date().toISOString() },
artifacts: [],
history: [],
metadata: { some_field: null }, // <-- valid per spec, real-world example: a numeric field that's legitimately absent
}
A2AService.sendMessage.responseSerialize({ payload: { $case: 'task', value: fakeTask } })
Observed
Error: invalid int32: object
at assertInt32 (@bufbuild/protobuf/dist/esm/wire/binary-encoding.js:481:15)
at BinaryWriter.int32 (@bufbuild/protobuf/dist/esm/wire/binary-encoding.js:176:9)
at Object.encode (chunk-QQCCX2KH.js:141:26) // Value.encode(), nullValue case
at Object.encode (chunk-QQCCX2KH.js:100:13) // Struct_FieldsEntry.encode()
at chunk-QQCCX2KH.js:42:28 // Struct.encode()'s forEach
at Array.forEach (<anonymous>)
at Object.encode (chunk-QQCCX2KH.js:40:36)
at Object.encode (chunk-QQCCX2KH.js:428:14) // Task.encode()
at Object.encode (chunk-QQCCX2KH.js:3060:14) // SendMessageResponse.encode()
at Object.responseSerialize (chunk-QQCCX2KH.js:3217:67)
Root cause
In the generated bindings:
// Value.wrap()
wrap(value) {
const result = createBaseValue();
if (value === null) {
result.kind = { $case: "nullValue", value }; // <-- sets value: null, should be 0
}
...
// Value.encode()
case "nullValue":
writer.uint32(8).int32(message.kind.value); // message.kind.value is literal `null` here
break;
@bufbuild/protobuf's BinaryWriter.int32() calls assertInt32, which rejects anything that isn't a number — null fails typeof value === 'number', hence the thrown error.
Suggested fix
In Value.wrap(), when value === null, set result.kind = { $case: "nullValue", value: 0 } (the NullValue.NULL_VALUE enum member), not the literal JS null.
Environment
@a2a-js/sdk: 0.3.13
@bufbuild/protobuf: (whatever is pinned as a peer/transitive dep at the time of testing)
- Node.js 26.5.0
- Encountered via the sdk's gRPC server transport (
@a2a-js/sdk/server/grpc), triggered by two independent downstream services returning a Task/tool-result with a legitimately-null metadata field over gRPC.
Summary
Value.wrap()in the generated gRPC protobuf bindings (dist/chunk-*.js, compiled fromsrc/grpc/pb/google/protobuf/struct.ts) sets thenullValueoneof case to the literal JSnullinstead of theNullValue.NULL_VALUEenum value (0).@bufbuild/protobuf's wire encoder strictly validates int32 fields and throws when it receivesnull, so anyTask/Message(or other Struct-typed) field containing a JSONnullvalue crashes gRPC serialization withError: invalid int32: object.The A2A spec defines
metadataas "a flexible key-value map ... values can be any valid value that can be represented in JSON" —nullis valid JSON, so this isn't a caller error, it's a bug in the wire encoding of the well-knownStruct/Valuetypes.This is a known class of pitfall in ts-proto-generated code for
google.protobuf.NullValue(see stephenh/ts-proto#458) — the fix is to mapnullto0(not passnullstraight through) before writing the int32 field.Reproduction
Observed
Root cause
In the generated bindings:
@bufbuild/protobuf'sBinaryWriter.int32()callsassertInt32, which rejects anything that isn't a number —nullfailstypeof value === 'number', hence the thrown error.Suggested fix
In
Value.wrap(), whenvalue === null, setresult.kind = { $case: "nullValue", value: 0 }(theNullValue.NULL_VALUEenum member), not the literal JSnull.Environment
@a2a-js/sdk: 0.3.13@bufbuild/protobuf: (whatever is pinned as a peer/transitive dep at the time of testing)@a2a-js/sdk/server/grpc), triggered by two independent downstream services returning aTask/tool-result with a legitimately-null metadata field over gRPC.