Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 18, 2025

Fix ToJsonObject serialization failure with anonymous types

Fixes a regression in 0.5.0-preview.1 where AIContent.AdditionalProperties containing anonymous types (e.g., from Semantic Kernel sampling) would fail with NotSupportedException during serialization.

Changes Made

  • Fixed serialization of anonymous types: Changed ToJsonObject to use McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IReadOnlyDictionary<string, object?>)) instead of strongly-typed JsonContext.Default.IReadOnlyDictionaryStringObject, allowing the resolver chain to handle user-defined types through reflection when available.

  • Added optional JsonSerializerOptions parameter: Added optional JsonSerializerOptions parameter to ToContentBlock, ToJsonObject, and ToChatMessage methods, enabling users to pass custom serialization options (converters, naming policies, type resolvers) that are properly applied throughout the serialization pipeline.

  • Updated serialization calls: All JSON serialization operations within ToContentBlock and ToChatMessage now use the provided options parameter, ensuring consistent behavior for:

    • FunctionCallContent argument serialization
    • FunctionResultContent nested content serialization
    • Default case fallback serialization
    • AdditionalProperties serialization via ToJsonObject
    • CallToolResult serialization in ToChatMessage
  • Comprehensive testing: Added tests in AIContentExtensionsTests.cs and McpClientToolTests.cs covering:

    • Anonymous types in AdditionalProperties (skipped when reflection is disabled)
    • Custom JsonSerializerOptions propagation
    • Named user-defined types with source generation support
    • Nested anonymous types and mixed type scenarios
    • CallToolAsync with anonymous type arguments
    • ToChatMessage with anonymous types in ContentBlock.Meta

Behavior

Anonymous types in AdditionalProperties work when reflection-based serialization is enabled:

  • ✅ .NET 8: Reflection enabled by default
  • ✅ .NET 10: Reflection enabled by default
  • ⚠️ .NET 9: Reflection disabled by default (can be enabled via configuration)
  • ⚠️ AOT scenarios: Reflection unavailable (anonymous types not supported)

Named user-defined types work universally with source generation:

  • ✅ All .NET versions (8, 9, 10)
  • ✅ Works regardless of reflection being enabled/disabled
  • ✅ AOT-compatible when properly registered with [JsonSerializable] attributes

Users can now pass custom serialization options to both ToContentBlock and ToChatMessage:

var customOptions = new JsonSerializerOptions 
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
var block = content.ToContentBlock(customOptions);
var message = result.ToChatMessage("call_123", customOptions);

For named types with source generation:

var options = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions);
options.TypeInfoResolverChain.Add(MyJsonContext.Default);
var block = content.ToContentBlock(options);

Testing

  • ✅ All 16 AIContentExtensions tests passing on .NET 8, 9, and 10
  • ✅ Tests verify custom options are properly used
  • ✅ Tests demonstrate named types work with source generation regardless of reflection availability
  • ✅ Tests verify anonymous types work in CallToolAsync arguments and ToChatMessage serialization
  • ✅ Backward compatible (optional parameter with default value)

The tests confirm that user-defined types in object graphs are properly handled through the resolver chain, and user-defined JsonSerializerOptions are consistently applied throughout all serialization paths.

Original prompt

This section details on the original issue you should resolve

<issue_title>ToJsonObject fails when dictionary values contain anonymous types (sampling pipeline regression in 0.5.0-preview.1)</issue_title>
<issue_description>Describe the bug
When using Semantic Kernel sampling via MCP, the internal payload passed from Semantic Kernel to MCP contains a dictionary whose values may include anonymous types (objects generated by Semantic Kernel and not controllable by the application).

In ModelContextProtocol 0.5.0-preview.1, calling
ModelContextProtocol.AIContentExtensions.ToJsonObject(IReadOnlyDictionary<string, object?> properties)
throws a System.NotSupportedException when any dictionary value is an anonymous type.

This previously worked in 0.4.0-preview.3 (stdio transport only) but now fails in 0.5.0-preview.1, including stdio, indicating that stdio and web transports now share the same serialization pipeline.

To Reproduce
Use Semantic Kernel with MCP sampling (AddSamplingChatClient)

Trigger a prompt invocation where Semantic Kernel generates arguments/metadata internally

Ensure the dictionary passed into
ModelContextProtocol.AIContentExtensions.ToJsonObject(...)
contains any anonymous type as a value (structure does not matter)

Run with ModelContextProtocol 0.5.0-preview.1

Observe serialization failure

Minimal conceptual example:

var dict = new Dictionary<string, object?>
{
["data"] = new { X = 1.0, Y = 2.0 }
};

AIContentExtensions.ToJsonObject(dict);

Expected behavior
ToJsonObject should be able to serialize dictionaries whose values are anonymous types, producing a valid JsonObject / JsonNode.

At minimum, anonymous types should be handled as plain object graphs rather than requiring source-generated JsonTypeInfo, which cannot be provided for anonymous types.

This is especially important because:

These anonymous types are generated internally by Semantic Kernel

Application developers cannot intercept or replace them

Logs

System.NotSupportedException: JsonTypeInfo metadata for type
'<>f__AnonymousType0`5[...]' was not provided by TypeInfoResolver
of type 'ModelContextProtocol.McpJsonUtilities+JsonContext'
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetadataForType
   at System.Text.Json.JsonSerializer.SerializeToNode
   at ModelContextProtocol.AIContentExtensions.ToJsonObject
   at ModelContextProtocol.Server.McpServer.SampleAsync(...)

Additional context

In 0.4.0-preview.3:

stdio transport worked

web transport failed (likely due to different serialization pipelines)

In 0.5.0-preview.1:

Both stdio and web transports fail

Indicates a unified pipeline that now enforces the same resolver behavior

Request / suggestion:
Enhance ModelContextProtocol.AIContentExtensions.ToJsonObject to support
dictionary values containing anonymous types, either by:

Falling back to a non–source-generated resolver for such values, or

Detecting anonymous types and serializing them via a compatible JsonSerializerOptions

This would restore compatibility with Semantic Kernel sampling scenarios
where developers cannot control the generated object shapes.</issue_description>

Comments on the Issue (you are @copilot in this section)

@eiriktsarpalis Have you turned on `PublishAOT` in your application? Unfortunately, anonymous types cannot be (and probably never will be) supported by source generators. You could either turn off AOT or replace your anonymous types with equivalent named types (it should be straightforward nowadays with record types). @stephentoub @eiriktsarpalis, this fails without PublishAOT. Here's a minimal repro: ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol;

AIContent c = new()
{
AdditionalProperties = new()
{
["data"] = new { X = 1.0, Y = 2.0 }
}
};

c.ToContentBlock();

That fails with:
```csharp
Unhandled exception. System.NotSupportedException: JsonTypeInfo metadata for type '<>f__AnonymousType0`2[System.Double,System.Double]' was not provided by TypeInfoResolver of type 'ModelContextProtocol.McpJsonUtilities+JsonContext'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically. The unsupported member type is located on type 'System.Object'. Path: $.
 ---> System.NotSupportedException: JsonTypeInfo metadata for type '<>f__AnonymousType0`2[System.Double,System.Double]' was not provided by TypeInfoResolver of type 'ModelContextProtocol.McpJsonUtilities+JsonContext'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.
   at System.Text.Json.ThrowHelper.ThrowNotSupportedException_NoMetad...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes modelcontextprotocol/csharp-sdk#1112

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/modelcontextprotocol/csharp-sdk/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits December 18, 2025 23:27
- Updated ToJsonObject to use JsonSerializer.SerializeToNode(object, Type, JsonSerializerOptions) instead of strongly-typed JsonTypeInfo
- Added DefaultJsonTypeInfoResolver to McpJsonUtilities.DefaultOptions to enable reflection-based serialization for user-defined types
- Fixed bug in FunctionResultContent serialization where it was serializing 'content' instead of 'resultContent.Result'
- Simplified default case in ToContentBlock to avoid serializing unsupported AIContent types
- Added comprehensive tests for anonymous types in AdditionalProperties
- All tests passing on .NET 8, 9, and 10

Co-authored-by: stephentoub <[email protected]>
- Updated test to verify that DefaultOptions now supports anonymous types unconditionally
- Test reflects that DefaultJsonTypeInfoResolver is always added to support user-defined types

Co-authored-by: stephentoub <[email protected]>
- Use more efficient JsonSerializer.Serialize overload that takes Type parameter directly
- Add suppression attributes to ToContentBlock method for trim/AOT warnings

Co-authored-by: stephentoub <[email protected]>
Copilot AI changed the title [WIP] Fix ToJsonObject when dictionary contains anonymous types Fix ToJsonObject serialization failure with anonymous types in dictionary values Dec 18, 2025
Copilot AI requested a review from stephentoub December 18, 2025 23:44
…ntext properties

- Changed ToJsonObject to use McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IReadOnlyDictionary<string, object?>))
- Changed ToolUseContentBlock deserialization to use GetTypeInfo<IDictionary<string, object?>>()
- Removed UnconditionalSuppressMessage attributes (not needed)
- Reverted McpJsonUtilities to original (no DefaultJsonTypeInfoResolver)
- Reverted FunctionResultContent and default case serialization to original code
- Updated tests to skip when reflection is disabled (JsonSerializer.IsReflectionEnabledByDefault)

This fix allows anonymous types to work when reflection is enabled (default on .NET 8/10 and opt-in on .NET 9) while maintaining AOT compatibility with source generators.

Co-authored-by: stephentoub <[email protected]>
- Consolidated all anonymous type tests into AIContentExtensionsTests.cs
- Removed AIContentExtensionsAnonymousTypeTests.cs and RegressionTests.cs
- Tests are now colocated with other content block tests as requested

Co-authored-by: stephentoub <[email protected]>
…JsonObject

- Added optional JsonSerializerOptions parameter to ToContentBlock method
- Added optional JsonSerializerOptions parameter to ToJsonObject helper method
- Updated all serialization calls within ToContentBlock to use provided options
- Updated XML documentation references in McpServerTool and McpServerToolAttribute
- Added test to verify custom JsonSerializerOptions are properly used
- Enables user-defined serialization options to be passed through for anonymous types and custom converters

Co-authored-by: eiriktsarpalis <[email protected]>
Copilot AI changed the title Fix ToJsonObject serialization failure with anonymous types in dictionary values Fix ToJsonObject serialization failure with anonymous types and add support for custom JsonSerializerOptions Dec 19, 2025
Copilot AI requested a review from eiriktsarpalis December 19, 2025 10:05
- Added ToContentBlock_WithNamedUserDefinedTypeInAdditionalProperties_Works test
- Test demonstrates that named types work with source generation regardless of reflection being enabled
- Created TestCoordinates record and NamedTypeTestJsonContext for source generation support
- Test passes on all frameworks including .NET 9 where reflection is disabled by default

Co-authored-by: eiriktsarpalis <[email protected]>
@stephentoub
Copy link
Contributor

@copilot, the concerns here apply not just to direct serialization and deserialization of dictionaries, but also types that contain dictionaries. For example, can you validate that anonymous types work as argument values iin a CallToolAsync call? Please review all serialization and deserialization calls that are using DefaultContext, looking for other places where user types might be in the object graph, test that there's indeed a problem with each, add tests for each (to existing files), and then fix each.

@stephentoub stephentoub marked this pull request as ready for review December 19, 2025 12:37
- Added test for CallToolAsync with anonymous type arguments
- Added test for ToChatMessage with anonymous types in ContentBlock.Meta
- Added ArgumentEchoTool to McpClientToolTests to support testing
- Tests verify that anonymous types work correctly when reflection is enabled
- All tests pass, indicating AIJsonUtilities resolver chain handles anonymous types

The tests demonstrate that user-defined types in dictionaries are properly serialized
through the existing resolver chain that includes AIJsonUtilities.DefaultOptions.

Co-authored-by: stephentoub <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 19, 2025

@copilot, the concerns here apply not just to direct serialization and deserialization of dictionaries, but also types that contain dictionaries. For example, can you validate that anonymous types wor...

Added tests for CallToolAsync with anonymous type arguments and ToChatMessage with anonymous types in ContentBlock.Meta in commit cb95813. All tests pass on .NET 8, 9, and 10.

Investigation findings:

  • ToArgumentsDictionary in McpClient.Methods.cs uses options.GetTypeInfo<object?>() to serialize dictionary values
  • ToChatMessage uses JsonContext.Default.CallToolResult for serialization
  • Tests confirm anonymous types work correctly due to the resolver chain that includes AIJsonUtilities.DefaultOptions

The existing implementation appears to handle user-defined types in object graphs correctly through the chained type resolvers. The ToContentBlock and ToJsonObject methods now accept optional JsonSerializerOptions parameters to allow users to provide custom resolvers when needed.

@stephentoub
Copy link
Contributor

@copilot, regarding:

ToChatMessage uses JsonContext.Default.CallToolResult for serialization

Doesn't that mean there could be problems with ToChatMessage? JsonContext.Default.CallToolResult will not see the chained in default resolver, right?

- Updated ToChatMessage to accept optional JsonSerializerOptions parameter
- Changed from using JsonContext.Default.CallToolResult to options.GetTypeInfo<CallToolResult>()
- Ensures user-defined serialization options are used throughout the call chain
- Defaults to McpJsonUtilities.DefaultOptions when no options provided
- All tests passing on .NET 8, 9, and 10

Co-authored-by: stephentoub <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 19, 2025

@copilot, regarding:

ToChatMessage uses JsonContext.Default.CallToolResult for serialization
...

You're correct. Using JsonContext.Default.CallToolResult bypasses the chained resolver. Fixed in commit a3cb705 by adding an optional JsonSerializerOptions parameter to ToChatMessage and using options.GetTypeInfo<CallToolResult>() instead, ensuring user-defined resolvers are properly applied.

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.

3 participants