Skip to content

fix(auth): accept raw security scheme JSON from non-Go SDKs (fixes #347)#358

Closed
kuangmi-bit wants to merge 3 commits into
a2aproject:mainfrom
kuangmi-bit:fix/java-jackson-agentcard-compat
Closed

fix(auth): accept raw security scheme JSON from non-Go SDKs (fixes #347)#358
kuangmi-bit wants to merge 3 commits into
a2aproject:mainfrom
kuangmi-bit:fix/java-jackson-agentcard-compat

Conversation

@kuangmi-bit

Copy link
Copy Markdown
Contributor

Problem

Java SDKs using Jackson serialization produce AgentCard JSON where security schemes omit the discriminator wrapper expected by a2a-go:

// Jackson (Spring Boot default) — no discriminator
"oauth2SecurityScheme": {
  "flows": {
    "clientCredentials": {
      "scopes": {...},
      "tokenUrl": "..."
    }
  },
  "description": "...",
  "oauth2MetadataUrl": null
}

a2a-go's NamedSecuritySchemes.UnmarshalJSON requires the discriminator:

// Gson (Go/Java SDK internal) — with discriminator ✓
"oauth2SecurityScheme": {
  "oauth2SecurityScheme": {
    "flows": {...}
  }
}

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:

Field → Type
flows OAuth2SecurityScheme
location APIKeySecurityScheme
scheme HTTPAuthSecurityScheme
openIdConnectUrl OpenIDConnectSecurityScheme

MutualTLS is excluded — it has no distinctive non-optional fields.

Verification

Fixes #347

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

@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 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.

Comment thread a2a/auth.go Outdated
Comment on lines +158 to +175
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)))

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.

medium

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
  1. Prefer explicit implementation over reusing a helper function if the helper's side effects are not obvious and the explicit implementation improves clarity.

Comment thread a2a/auth.go Outdated
Comment on lines +186 to +245
// 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
}

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.

medium

These helper functions are no longer needed if we inline the parsing logic in UnmarshalJSON as suggested. Removing them simplifies the codebase and improves maintainability.

kuangmi added 2 commits June 17, 2026 17:37
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.
@kuangmi-bit

Copy link
Copy Markdown
Contributor Author

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 securitySchemes without the A2A discriminator wrapper — {"oauth2SecurityScheme": {"flows": ...}} instead of {"oauth2SecurityScheme": {"oauth2SecurityScheme": {"flows": ...}}}.

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 map[string]json.RawMessage unmarshal → key-based dispatch → at most 2 unmarshals total.

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!

@nahapetyan-serob

Copy link
Copy Markdown
Collaborator

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 MutualTLS don't have unique, mandatory fields, a field-matching fallback can't safely parse every scheme type. We'd end up with weird, inconsistent behavior—and baking in partial workarounds for specific backend serialization bugs adds maintenance debt to what should be a strictly spec-compliant core.

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
}

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]: AgentCard Defined By Java Server cannot be Serialized in Go SDK

3 participants