Skip to content

fix: key A2AJsonConverter safe-options cache per JsonSerializerOptions instance - #436

Open
chopmob-cloud wants to merge 1 commit into
a2aproject:mainfrom
chopmob-cloud:fix/issue-298-jsonoptions-cache
Open

fix: key A2AJsonConverter safe-options cache per JsonSerializerOptions instance#436
chopmob-cloud wants to merge 1 commit into
a2aproject:mainfrom
chopmob-cloud:fix/issue-298-jsonoptions-cache

Conversation

@chopmob-cloud

Copy link
Copy Markdown

Summary

Fixes #298. A2AJsonConverter<T>.GetSafeOptions cached its converter-stripped options in two static fields, on the documented assumption that A2A uses a single JsonSerializerOptions instance for the process lifetime. When a second, distinct instance reached it, a guard threw. In Release Debug.Fail is compiled out, so throw new InvalidOperationException() ran with its default message "Operation is not valid due to the current state of the object", surfaced by Write as A2AException: Failed to serialize FileContent: .... Because the fields are per closed generic, the first instance poisoned the cache for every later one.

A host layer such as Microsoft.Agents.AI.Hosting builds its own options from A2AJsonUtilities.DefaultOptions with a rebuilt resolver chain. That is a second, distinct instance, and it is what tripped the guard in the reported traces (confirmed in the issue thread reproducing on the current preview line via the A2A.V0_3Compat path).

Fix

Replace the single-global-instance cache with a ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> keyed on the caller options instance, so each instance gets its own converter-stripped clone. The table is reference-keyed (JsonSerializerOptions has no value equality) and its entries are collected together with the caller options, so the cache neither collides across instances nor leaks. The guard and Debug.Fail are removed. No public API changes.

Validation

  • New tests/A2A.V0_3.UnitTests/GitHubIssues/Issue298.cs reproduces the throw on the pre-fix converter (fails with the exact A2AException) and round-trips cleanly with the fix.
  • Full suites pass on both target frameworks in Release: A2A.V0_3.UnitTests 263/263 on net8.0 and net10.0, and A2A.UnitTests 420/420 on net8.0 and net10.0.

@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 the caching mechanism in A2AJsonConverter<T>.GetSafeOptions to use a ConditionalWeakTable instead of a single static field, resolving an issue where using multiple distinct JsonSerializerOptions instances caused an InvalidOperationException. A unit test was also added to verify this behavior. The review feedback suggests calling clone.MakeReadOnly() on the cloned options before caching them to ensure they are frozen and thread-safe.

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 src/A2A.V0_3/A2AJsonConverter.cs
…s instance (a2aproject#298)

A2AJsonConverter<T>.GetSafeOptions cached its converter-stripped options in two
static fields, on the assumption that A2A uses a single JsonSerializerOptions
instance for the process lifetime. When a second, distinct instance reached it
(for example a host layer such as Microsoft.Agents.AI.Hosting that builds its own
options from A2AJsonUtilities.DefaultOptions with a rebuilt resolver chain), the
guard threw InvalidOperationException. In Release Debug.Fail is compiled out, so
the throw ran with its default message "Operation is not valid due to the current
state of the object", surfaced by Write as
"A2AException: Failed to serialize FileContent: ...". The static fields are per
closed generic, so the first instance poisoned the cache for every later one.

Replace the single-global-instance cache with a ConditionalWeakTable keyed on the
caller JsonSerializerOptions instance, so each instance gets its own
converter-stripped clone. The table is reference-keyed (JsonSerializerOptions has
no value equality) and its entries are collected together with the caller options,
so the cache neither collides across instances nor leaks. The guard and Debug.Fail
are removed.

Adds tests/A2A.V0_3.UnitTests/GitHubIssues/Issue298.cs, which reproduces the
throw on the pre-fix converter and round-trips cleanly with the fix.
@chopmob-cloud
chopmob-cloud force-pushed the fix/issue-298-jsonoptions-cache branch from 6304a22 to 17c4551 Compare July 15, 2026 14:22
@chopmob-cloud

Copy link
Copy Markdown
Author

Good call, adopted. Freezing the cached clone with MakeReadOnly() matches A2AJsonUtilities.DefaultOptions and closes any window where the shared per-instance copy could be mutated after publication. Tests stay green on both target frameworks.

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]: FileContent deserialization poisons static JsonSerializerOptions cache, breaking all subsequent serializations

1 participant