diff --git a/src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs b/src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs index 87bb430df..617b33359 100644 --- a/src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs +++ b/src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs @@ -5,8 +5,8 @@ using System.Diagnostics.CodeAnalysis; #if !NET9_0_OR_GREATER using System.Reflection; -using System.Text.Json; #endif +using System.Text.Json; using System.Text.Json.Serialization; #if !NET9_0_OR_GREATER using ModelContextProtocol; @@ -66,6 +66,31 @@ public override string ConvertName(string name) => } #endif } + + /// + /// A JSON converter for enums that allows customizing the serialized string value of enum members + /// using the . + /// + /// + /// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter<T> + /// 9.x support for custom enum member naming. It will be replaced by the built-in functionality + /// once .NET 9 is fully adopted. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [RequiresUnreferencedCode("Requires unreferenced code to instantiate the generic enum converter.")] + [RequiresDynamicCode("Requires dynamic code to instantiate the generic enum converter.")] + public sealed class CustomizableJsonStringEnumConverter : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum; + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type converterType = typeof(CustomizableJsonStringEnumConverter<>).MakeGenericType(typeToConvert)!; + var factory = (JsonConverterFactory)Activator.CreateInstance(converterType)!; + return factory.CreateConverter(typeToConvert, options); + } + } } #if !NET9_0_OR_GREATER diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 0bc9ee777..b075e1263 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -2,6 +2,7 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -34,14 +35,22 @@ public static partial class McpJsonUtilities /// Creates default options to use for MCP-related serialization. /// /// The configured options. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] private static JsonSerializerOptions CreateDefaultOptions() { // Copy the configuration from the source generated context. JsonSerializerOptions options = new(JsonContext.Default.Options); - // Chain with all supported types from MEAI + // Chain with all supported types from MEAI. options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!); + // Add a converter for user-defined enums, if reflection is enabled by default. + if (JsonSerializer.IsReflectionEnabledByDefault) + { + options.Converters.Add(new CustomizableJsonStringEnumConverter()); + } + options.MakeReadOnly(); return options; } diff --git a/tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs b/tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs index 18dc680ef..e0af61eed 100644 --- a/tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs +++ b/tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol.Tests; @@ -22,4 +24,27 @@ public static void DefaultOptions_UseReflectionWhenEnabled() Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, options.TryGetTypeInfo(anonType, out _)); } + + [Fact] + public static void DefaultOptions_UnknownEnumHandling() + { + var options = McpJsonUtilities.DefaultOptions; + + if (JsonSerializer.IsReflectionEnabledByDefault) + { + Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithoutAnnotation.A, options)); + Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithAnnotation.A, options)); + } + else + { + options = new(options) { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; + Assert.Equal("1", JsonSerializer.Serialize(EnumWithoutAnnotation.A, options)); + Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithAnnotation.A, options)); + } + } + + public enum EnumWithoutAnnotation { A = 1, B = 2, C = 3 } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum EnumWithAnnotation { A = 1, B = 2, C = 3 } }