Skip to content

Commit 1e8fb04

Browse files
Make enums serialize as strings if using the reflection-based serializer. (#473)
* Make enums serialize as strings if using the reflection-based serializer. * Remove unnecessary partial * Use an explicit reflection-based converter for enums. * Update comments * Make the reflection-based enum converter public.
1 parent d7d9eab commit 1e8fb04

File tree

3 files changed

+61
-2
lines changed

3 files changed

+61
-2
lines changed

src/ModelContextProtocol.Core/CustomizableJsonStringEnumConverter.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
using System.Diagnostics.CodeAnalysis;
66
#if !NET9_0_OR_GREATER
77
using System.Reflection;
8-
using System.Text.Json;
98
#endif
9+
using System.Text.Json;
1010
using System.Text.Json.Serialization;
1111
#if !NET9_0_OR_GREATER
1212
using ModelContextProtocol;
@@ -66,6 +66,31 @@ public override string ConvertName(string name) =>
6666
}
6767
#endif
6868
}
69+
70+
/// <summary>
71+
/// A JSON converter for enums that allows customizing the serialized string value of enum members
72+
/// using the <see cref="JsonStringEnumMemberNameAttribute"/>.
73+
/// </summary>
74+
/// <remarks>
75+
/// This is a temporary workaround for lack of System.Text.Json's JsonStringEnumConverter&lt;T&gt;
76+
/// 9.x support for custom enum member naming. It will be replaced by the built-in functionality
77+
/// once .NET 9 is fully adopted.
78+
/// </remarks>
79+
[EditorBrowsable(EditorBrowsableState.Never)]
80+
[RequiresUnreferencedCode("Requires unreferenced code to instantiate the generic enum converter.")]
81+
[RequiresDynamicCode("Requires dynamic code to instantiate the generic enum converter.")]
82+
public sealed class CustomizableJsonStringEnumConverter : JsonConverterFactory
83+
{
84+
/// <inheritdoc/>
85+
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
86+
/// <inheritdoc/>
87+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
88+
{
89+
Type converterType = typeof(CustomizableJsonStringEnumConverter<>).MakeGenericType(typeToConvert)!;
90+
var factory = (JsonConverterFactory)Activator.CreateInstance(converterType)!;
91+
return factory.CreateConverter(typeToConvert, options);
92+
}
93+
}
6994
}
7095

7196
#if !NET9_0_OR_GREATER

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ModelContextProtocol.Protocol;
33
using ModelContextProtocol.Server;
44
using System.Diagnostics.CodeAnalysis;
5+
using System.Reflection;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78
using System.Text.Json.Serialization.Metadata;
@@ -34,14 +35,22 @@ public static partial class McpJsonUtilities
3435
/// Creates default options to use for MCP-related serialization.
3536
/// </summary>
3637
/// <returns>The configured options.</returns>
38+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
39+
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
3740
private static JsonSerializerOptions CreateDefaultOptions()
3841
{
3942
// Copy the configuration from the source generated context.
4043
JsonSerializerOptions options = new(JsonContext.Default.Options);
4144

42-
// Chain with all supported types from MEAI
45+
// Chain with all supported types from MEAI.
4346
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
4447

48+
// Add a converter for user-defined enums, if reflection is enabled by default.
49+
if (JsonSerializer.IsReflectionEnabledByDefault)
50+
{
51+
options.Converters.Add(new CustomizableJsonStringEnumConverter());
52+
}
53+
4554
options.MakeReadOnly();
4655
return options;
4756
}

tests/ModelContextProtocol.Tests/McpJsonUtilitiesTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using System.Text.Json.Serialization.Metadata;
24

35
namespace ModelContextProtocol.Tests;
46

@@ -22,4 +24,27 @@ public static void DefaultOptions_UseReflectionWhenEnabled()
2224

2325
Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, options.TryGetTypeInfo(anonType, out _));
2426
}
27+
28+
[Fact]
29+
public static void DefaultOptions_UnknownEnumHandling()
30+
{
31+
var options = McpJsonUtilities.DefaultOptions;
32+
33+
if (JsonSerializer.IsReflectionEnabledByDefault)
34+
{
35+
Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithoutAnnotation.A, options));
36+
Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithAnnotation.A, options));
37+
}
38+
else
39+
{
40+
options = new(options) { TypeInfoResolver = new DefaultJsonTypeInfoResolver() };
41+
Assert.Equal("1", JsonSerializer.Serialize(EnumWithoutAnnotation.A, options));
42+
Assert.Equal("\"A\"", JsonSerializer.Serialize(EnumWithAnnotation.A, options));
43+
}
44+
}
45+
46+
public enum EnumWithoutAnnotation { A = 1, B = 2, C = 3 }
47+
48+
[JsonConverter(typeof(JsonStringEnumConverter<EnumWithAnnotation>))]
49+
public enum EnumWithAnnotation { A = 1, B = 2, C = 3 }
2550
}

0 commit comments

Comments
 (0)