fix(auth): accept raw security scheme JSON from non-Go SDKs (fixes #347)#358
fix(auth): accept raw security scheme JSON from non-Go SDKs (fixes #347)#358kuangmi-bit wants to merge 3 commits into
Conversation
Java SDKs using Jackson serialization may omit the discriminator wrapper (e.g., 'oauth2SecurityScheme' key inside the scheme object) and emit the raw fields directly (flows, description, etc.). Add a fallback path in NamedSecuritySchemes.UnmarshalJSON that detects missing discriminators and tries each known type based on distinctive JSON fields: - 'flows' → OAuth2SecurityScheme - 'location' → APIKeySecurityScheme - 'scheme' → HTTPAuthSecurityScheme - 'openIdConnectUrl' → OpenIDConnectSecurityScheme MutualTLS is excluded from the fallback because it has no distinctive non-optional fields and would falsely match arbitrary JSON. Add TestParseJavaJacksonAgentCard with the exact payload from a2aproject#347. Fixes a2aproject#347
There was a problem hiding this comment.
Code Review
This pull request introduces a fallback mechanism in a2a/auth.go to parse security schemes when no discriminator is found, addressing compatibility issues with non-Go SDKs (such as Java Jackson serialization). It also adds a compatibility test in a2a/java_compat_test.go. The review feedback suggests optimizing this fallback logic by unmarshaling the raw JSON into a map of fields once rather than repeatedly unmarshaling it inside multiple helper functions, which would improve performance and simplify the codebase by eliminating the helper functions.
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.
| rawJSON := raw[name] | ||
| if scheme, err := tryParseOAuth2(rawJSON); err == nil { | ||
| result[name] = scheme | ||
| continue | ||
| } | ||
| if scheme, err := tryParseAPIKey(rawJSON); err == nil { | ||
| result[name] = scheme | ||
| continue | ||
| } | ||
| if scheme, err := tryParseHTTPAuth(rawJSON); err == nil { | ||
| result[name] = scheme | ||
| continue | ||
| } | ||
| if scheme, err := tryParseOpenIDConnect(rawJSON); err == nil { | ||
| result[name] = scheme | ||
| continue | ||
| } | ||
| return fmt.Errorf("unknown security scheme type for %s: %v", name, jsonKeys([]byte(rawJSON))) |
There was a problem hiding this comment.
The current implementation is highly inefficient because it repeatedly unmarshals the same JSON fragment (rawJSON) into a map (via jsonHasKey inside each tryParse* function) and then unmarshals it again into the concrete struct. In the worst case, this results in up to 6 unmarshal operations for a single scheme.
By unmarshaling rawJSON into a map of fields once, we can check the keys directly and unmarshal into the matched struct. This reduces the number of unmarshal operations to at most 2, improves performance, and allows us to completely eliminate the helper functions tryParseOAuth2, tryParseAPIKey, tryParseHTTPAuth, tryParseOpenIDConnect, and jsonHasKey.
rawJSON := raw[name]
var fields map[string]json.RawMessage
if err := json.Unmarshal(rawJSON, &fields); err != nil {
return fmt.Errorf("unknown security scheme for %s: %w", name, err)
}
if _, ok := fields["flows"]; ok {
var scheme OAuth2SecurityScheme
if err := json.Unmarshal(rawJSON, &scheme); err != nil {
return err
}
result[name] = scheme
continue
}
if _, ok := fields["location"]; ok {
var scheme APIKeySecurityScheme
if err := json.Unmarshal(rawJSON, &scheme); err != nil {
return err
}
result[name] = scheme
continue
}
if _, ok := fields["scheme"]; ok {
var scheme HTTPAuthSecurityScheme
if err := json.Unmarshal(rawJSON, &scheme); err != nil {
return err
}
result[name] = scheme
continue
}
if _, ok := fields["openIdConnectUrl"]; ok {
var scheme OpenIDConnectSecurityScheme
if err := json.Unmarshal(rawJSON, &scheme); err != nil {
return err
}
result[name] = scheme
continue
}
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
return fmt.Errorf("unknown security scheme type for %s: %v", name, keys)References
- Prefer explicit implementation over reusing a helper function if the helper's side effects are not obvious and the explicit implementation improves clarity.
| // tryParseDirect attempts to unmarshal raw JSON as a concrete security scheme type. | ||
| // Unlike the discriminator-based approach, this works with raw scheme objects from | ||
| // non-Go SDKs (e.g., Java Jackson) that omit the discriminator wrapper. | ||
| // | ||
| // Each tryParse* function checks for the presence of a distinctive field before | ||
| // attempting full unmarshal, avoiding false positives on empty/ambiguous payloads. | ||
|
|
||
| func tryParseOAuth2(raw json.RawMessage) (SecurityScheme, error) { | ||
| if !jsonHasKey(raw, "flows") { | ||
| return nil, errors.New("not OAuth2: missing 'flows'") | ||
| } | ||
| var s OAuth2SecurityScheme | ||
| if err := json.Unmarshal(raw, &s); err != nil { | ||
| return nil, err | ||
| } | ||
| return s, nil | ||
| } | ||
|
|
||
| func tryParseAPIKey(raw json.RawMessage) (SecurityScheme, error) { | ||
| if !jsonHasKey(raw, "location") { | ||
| return nil, errors.New("not APIKey: missing 'location'") | ||
| } | ||
| var s APIKeySecurityScheme | ||
| if err := json.Unmarshal(raw, &s); err != nil { | ||
| return nil, err | ||
| } | ||
| return s, nil | ||
| } | ||
|
|
||
| func tryParseHTTPAuth(raw json.RawMessage) (SecurityScheme, error) { | ||
| if !jsonHasKey(raw, "scheme") { | ||
| return nil, errors.New("not HTTPAuth: missing 'scheme'") | ||
| } | ||
| var s HTTPAuthSecurityScheme | ||
| if err := json.Unmarshal(raw, &s); err != nil { | ||
| return nil, err | ||
| } | ||
| return s, nil | ||
| } | ||
|
|
||
| func tryParseOpenIDConnect(raw json.RawMessage) (SecurityScheme, error) { | ||
| if !jsonHasKey(raw, "openIdConnectUrl") { | ||
| return nil, errors.New("not OpenIDConnect: missing 'openIdConnectUrl'") | ||
| } | ||
| var s OpenIDConnectSecurityScheme | ||
| if err := json.Unmarshal(raw, &s); err != nil { | ||
| return nil, err | ||
| } | ||
| return s, nil | ||
| } | ||
|
|
||
| // jsonHasKey reports whether the JSON object contains a top-level key. | ||
| func jsonHasKey(raw json.RawMessage, key string) bool { | ||
| var m map[string]json.RawMessage | ||
| if err := json.Unmarshal(raw, &m); err != nil { | ||
| return false | ||
| } | ||
| _, ok := m[key] | ||
| return ok | ||
| } |
Replace the tryParse* + jsonHasKey chain with a single map[string]json.RawMessage unmarshal and a switch dispatch. Previously, unmarshalling the same rawJSON happened up to 4 times per fallback path (each jsonHasKey call + each struct unmarshal). Now it's at most 2: one map unmarshal to find the discriminator field, and one struct unmarshal for the matched type. This addresses the Gemini Code Assist review feedback about redundant unmarshal overhead in the non-Go SDK compat path. Also removes the now-unused tryParse* functions and jsonHasKey helper, and drops the stale 'errors' import.
|
Hi @nahapetyan-serob — another two-week-old MERGEABLE PR needing human review. Only gemini-code-assist has looked at it so far. This is the fix for #347 (Java SDK AgentCard interop). The root cause is that Jackson (Spring Boot default) serializes The bot flagged a performance concern (up to 6 unmarshals per scheme in worst case), which is now addressed in the follow-up commit: single Note: liuzemei opened #352 targeting the same issue. My approach differs in that it keeps the existing discriminator path as primary and adds the raw-JSON fallback as a secondary path, rather than replacing the unmarshaling logic entirely. Happy to discuss which approach the maintainers prefer — I can adjust based on your feedback. Thanks! |
|
Thanks for putting this together and trying to smooth over the Jackson serialization headache! We've decided not to pull this fallback logic into the core SDK. Because things like In cases where users can't change the backend to match the spec, they can use the SDK's built-in escape hatch and inject a custom parser directly: resolver := agentcard.Resolver{
CardParser: YourCustomParser, // Drop your lenient fallback parsing logic here
} |
Problem
Java SDKs using Jackson serialization produce AgentCard JSON where security schemes omit the discriminator wrapper expected by a2a-go:
a2a-go's
NamedSecuritySchemes.UnmarshalJSONrequires the discriminator:This causes cross-SDK interop failures when a Go sidecar fetches a Java server's AgentCard.
Fix
Added a fallback path in
UnmarshalJSON: when no discriminator is found, try each known concrete type based on distinctive JSON fields:flowslocationschemeopenIdConnectUrlMutualTLS is excluded — it has no distinctive non-optional fields.
Verification
TestParseJavaJacksonAgentCardwith the exact payload from [Bug]: AgentCard Defined By Java Server cannot be Serialized in Go SDK #347-race: all 18 packages OKFixes #347