Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 18, 2025

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 when using reflection-based serialization.

Changes Made

  • Fixed serialization with reflection: 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 parameters throughout the SDK: Added optional JsonSerializerOptions parameter to all public APIs that serialize or deserialize user-provided objects, enabling users to pass custom serialization options (converters, naming policies, type resolvers):

    • ToContentBlock - for converting AIContent to MCP protocol types
    • ToAIContent - for converting protocol types to AIContent
    • ToAIContents - for converting multiple content blocks
    • ToChatMessage - for converting protocol messages to chat messages
    • CreateSamplingHandler - for creating sampling handlers with custom serialization
    • AsSamplingChatClient - for creating sampling chat clients
    • McpServer.SampleAsync - for sampling operations with custom serialization
    • ToJsonObject (internal, required parameter) - for converting dictionaries to JsonObject
  • Complete serialization chain coverage: Updated all serialization and deserialization operations throughout the library to accept and properly pass through user-supplied JsonSerializerOptions, ensuring no location in the library hard-codes McpJsonUtilities.DefaultOptions when serializing arbitrary user-provided objects.

  • 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 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

Usage Examples

Users can now pass custom serialization options to control serialization behavior at any public API entry point, and those options will be consistently used throughout the entire call chain:

var customOptions = new JsonSerializerOptions 
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};

// Convert AIContent with custom options
var block = content.ToContentBlock(customOptions);

// Convert ContentBlock to AIContent with custom options
var aiContent = contentBlock.ToAIContent(customOptions);

// Convert CallToolResult with custom options
var message = result.ToChatMessage("call_123", customOptions);

// Create sampling handler with custom options
var handler = chatClient.CreateSamplingHandler(customOptions);

// Create sampling chat client with custom options
var samplingClient = chatClient.AsSamplingChatClient(customOptions);

// Sample with custom options
var response = await server.SampleAsync(messages, chatOptions, customOptions, cancellationToken);

For named types with source generation:

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

API Design

  • Public methods: Optional JsonSerializerOptions? parameters with default values for backward compatibility
  • Internal methods: Required JsonSerializerOptions parameters to ensure explicit passing throughout the codebase
  • Complete pass-through: User-supplied options are passed through the entire call chain from public APIs to all internal serialization operations

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 parameters with default values)

The changes enable comprehensive control over serialization throughout the MCP SDK while maintaining backward compatibility. Users can now supply custom JsonSerializerOptions at any public API entry point to control how their objects are serialized and deserialized throughout the entire SDK, enabling support for user-defined types (including anonymous types when reflection is available).

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 <2642209+stephentoub@users.noreply.github.com>
- 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 <2642209+stephentoub@users.noreply.github.com>
- 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 <2642209+stephentoub@users.noreply.github.com>
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 <2642209+stephentoub@users.noreply.github.com>
- 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 <2642209+stephentoub@users.noreply.github.com>
…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 <2813363+eiriktsarpalis@users.noreply.github.com>
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 <2813363+eiriktsarpalis@users.noreply.github.com>
@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.

…ct method

- Changed ToJsonObject parameter from optional to required (non-optional)
- Updated all call sites to explicitly pass JsonSerializerOptions
- Updated AIContentExtensions.cs line 78 to pass McpJsonUtilities.DefaultOptions
- Updated McpServer.Methods.cs lines 151 and 175 to pass McpJsonUtilities.DefaultOptions
- Ensures all locations explicitly specify serialization options
- All tests passing on .NET 8, 9, and 10

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
… and SampleAsync

- Added optional serializerOptions parameter to CreateSamplingHandler
- Added optional serializerOptions parameter to McpServer.SampleAsync(IEnumerable<ChatMessage>, ...)
- Updated all hard-coded McpJsonUtilities.DefaultOptions calls to use the parameter
- Updated call site in McpSamplingChatClient.GetResponseAsync to pass null explicitly
- Updated XML documentation reference in McpServerOptions
- Allows users to control serialization of user-provided objects (e.g., AdditionalProperties)
- All tests passing on .NET 8, 9, and 10

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
- Added optional JsonSerializerOptions parameter to ToAIContent(ContentBlock) and ToAIContent(ResourceContents)
- Added optional JsonSerializerOptions parameter to ToAIContents and ToChatMessage(PromptMessage)
- Updated CreateSamplingHandler to pass serializer options to ToChatClientArguments
- Updated ToChatClientArguments to accept and use serializer options when calling ToAIContent
- Updated AsSamplingChatClient to accept optional serializer options
- Updated SamplingChatClient to store and use serializer options instead of passing null
- Updated McpServer.SampleAsync to pass serializer options to ToAIContent
- Updated McpClientTool to pass JsonSerializerOptions to ToAIContent calls
- Updated test to use lambda instead of method group for AsSamplingChatClient
- Updated XML documentation references to reflect new method signatures
- All tests passing on .NET 8, 9, and 10

This ensures users can always supply custom JsonSerializerOptions for serialization operations throughout the library.

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@stephentoub stephentoub enabled auto-merge (squash) January 20, 2026 02:29
@stephentoub stephentoub requested a review from halter73 January 20, 2026 02:30
Copy link
Member

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot the PR title is somewhat misleading since we're not fixing serialization failures for anonymous types in general, we're just making them work in cases where reflection serialization is being used. Please update the title to reflect that we're simply passing user-defined serialization options in more places.

Copy link
Contributor Author

Copilot AI commented Jan 20, 2026

@copilot the PR title is somewhat misleading since we're not fixing serialization failures for anonymous types in general, we're just making them work in cases where reflection serialization is being ...

You're right. The PR is primarily about enabling user-defined JsonSerializerOptions to be passed through the serialization pipeline. Anonymous types work as a side effect when reflection is available. I cannot directly update the PR title, but the title should be changed to something like "Add support for custom JsonSerializerOptions throughout serialization pipeline" to better reflect the actual changes.

Copilot AI changed the title Fix ToJsonObject serialization failure with anonymous types and add support for custom JsonSerializerOptions Add support for user-defined JsonSerializerOptions throughout SDK serialization APIs Jan 20, 2026
Copilot AI requested a review from eiriktsarpalis January 20, 2026 09:14
@stephentoub stephentoub merged commit 46d3044 into main Jan 20, 2026
8 of 11 checks passed
@stephentoub stephentoub deleted the copilot/fix-tojsonobject-issue branch January 20, 2026 09:21
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.

4 participants