Skip to content

Conversation

@GeorgiPopovIT
Copy link

Added GetSerializerOptions() helper method in AIFunctionMcpServerTool and ensure JsonSerializerOptions have proper TypeInfoResolver configuration.

Executed tests and they passed.

Fixes #1150

@GeorgiPopovIT GeorgiPopovIT force-pushed the fix/json-options-without-typeinforesolver branch from f2b2deb to 4c0cdec Compare January 29, 2026 12:26
Added a helper method which ensures that custom options are properly configured with required TypeInfoResolvers.

Fixes modelcontextprotocol#1150
@GeorgiPopovIT GeorgiPopovIT force-pushed the fix/json-options-without-typeinforesolver branch from 4c0cdec to 61cb96a Compare January 29, 2026 13:14
}

customOptions.TypeInfoResolverChain.Add(McpJsonUtilities.JsonContext.Default);
customOptions.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
Copy link
Contributor

Choose a reason for hiding this comment

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

@eiriktsarpalis, is it desirable to mutate the externally-provided JsonSerializerOptions like this?

And is this thread-safe? What would happen if this JSO were passed to two McpServerTool.Create calls concurrently?

Copy link
Member

@eiriktsarpalis eiriktsarpalis Jan 29, 2026

Choose a reason for hiding this comment

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

Νο. And even if it was, it would fail assuming the passed instance were read-only.

Copy link
Contributor

Choose a reason for hiding this comment

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

@eiriktsarpalis, thanks. What is the right answer for #1150? We should support someone passing in new JsonSerializerOptions() { ... }.

Copy link
Member

Choose a reason for hiding this comment

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

It's using the copy constructor like the PR was doing originally. That does have the side-effect of creating a new JSO and repopulating caches every time the method is invoked.

Copy link
Contributor

Choose a reason for hiding this comment

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

How does JsonSerializer.Serialize(..., jso) avoid that? Or does it also create a new instance on each call?

Copy link
Member

Choose a reason for hiding this comment

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

Reading #1150 more closely, I believe the user expects that we replicate the semantics of the non-AOT friendly JsonSerializer.Serialize(JsonSerializerOptions) methods which do silently mutate the JsonSerializerOptions. We can approximate that behavior while preserving AOT compatibility as follows:

if (options.TypeInfoResolver is null)
{
    Debug.Assert(!options.IsReadOnly, "If no resolver is present then the options must still be editable");
    options.TypeInfoResolver = McpJsonUtilities.DefaultOptions.TypeInfoResolver;
    options.MakeReadOnly();
}

Copy link
Member

Choose a reason for hiding this comment

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

How does JsonSerializer.Serialize(..., jso) avoid that? Or does it also create a new instance on each call?

Just saw this but I think my last response should asnwer your question.

@stephentoub
Copy link
Contributor

Thanks for the efforts here. Replaced by #1221.

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.

Allow JsonSerializerOptions without TypeInfoResolver

3 participants