Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonEnumMemberNameAttribute. #105032

Merged
merged 14 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,12 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy =
public sealed override bool CanConvert(System.Type typeToConvert) { throw null; }
public sealed override System.Text.Json.Serialization.JsonConverter CreateConverter(System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Field, AllowMultiple=false)]
public partial class JsonStringEnumMemberNameAttribute : System.Attribute
{
public JsonStringEnumMemberNameAttribute(string name) { }
public string Name { get { throw null; } }
}
public enum JsonUnknownDerivedTypeHandling
{
FailSerialization = 0,
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.Converters.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.cs" />
<Compile Include="System\Text\Json\Serialization\JsonStringEnumConverter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonStringEnumMemberNameAttribute.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\FSharpCoreReflectionProxy.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonCollectionInfoValuesOfTCollection.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Collections.cs" />
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace System.Text.Json.Serialization.Converters
{
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
internal sealed class EnumConverterFactory : JsonConverterFactory
{
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
public EnumConverterFactory()
{
}
Expand All @@ -18,23 +18,42 @@ public override bool CanConvert(Type type)
return type.IsEnum;
}

[SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
Justification = "The constructor has been annotated with RequiredDynamicCodeAttribute.")]
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Debug.Assert(CanConvert(type));
return Create(type, EnumConverterOptions.AllowNumbers, namingPolicy: null, options);
}

internal static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
public static JsonConverter<T> Create<T>(EnumConverterOptions converterOptions, JsonSerializerOptions options, JsonNamingPolicy? namingPolicy = null)
where T : struct, Enum
{
return (JsonConverter)Activator.CreateInstance(
GetEnumConverterType(enumType),
new object?[] { converterOptions, namingPolicy, options })!;
if (Type.GetTypeCode(typeof(T)) is TypeCode.Char)
{
// Char-backed enums are valid in IL and F# but are not supported by System.Text.Json.
return new UnsupportedTypeConverter<T>();
}

return new EnumConverter<T>(converterOptions, namingPolicy, options);
}


[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
Justification = "'EnumConverter<T> where T : struct' implies 'T : new()', so the trimmer is warning calling MakeGenericType here because enumType's constructors are not annotated. " +
"But EnumConverter doesn't call new T(), so this is safe.")]
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private static Type GetEnumConverterType(Type enumType) => typeof(EnumConverter<>).MakeGenericType(enumType);
public static JsonConverter Create(Type enumType, EnumConverterOptions converterOptions, JsonNamingPolicy? namingPolicy, JsonSerializerOptions options)
{
if (Type.GetTypeCode(enumType) is TypeCode.Char)
{
// Char-backed enums are valid in IL and F# but are not supported by System.Text.Json.
return UnsupportedTypeConverterFactory.CreateUnsupportedConverterForType(enumType);
}

Type converterType = typeof(EnumConverter<>).MakeGenericType(enumType);
object?[] converterParams = [converterOptions, namingPolicy, options];
return (JsonConverter)Activator.CreateInstance(converterType, converterParams)!;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public JsonNumberEnumConverter() { }
ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
}

return new EnumConverter<TEnum>(EnumConverterOptions.AllowNumbers, options);
return EnumConverterFactory.Create<TEnum>(EnumConverterOptions.AllowNumbers, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allow
ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
}

return new EnumConverter<TEnum>(_converterOptions, _namingPolicy, options);
return EnumConverterFactory.Create<TEnum>(_converterOptions, options, _namingPolicy);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Text.Json.Serialization
{
/// <summary>
/// Determines the string value that should be used when serializing an enum member.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class JsonStringEnumMemberNameAttribute : Attribute
{
/// <summary>
/// Creates new attribute instance with a specified enum member name.
/// </summary>
/// <param name="name">The name to apply to the current enum member.</param>
public JsonStringEnumMemberNameAttribute(string name)
{
if (name is null)
{
ThrowHelper.ThrowArgumentNullException(nameof(name));
}

Name = name;
}

/// <summary>
/// Gets the name of the enum member.
/// </summary>
public string Name { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ public static JsonConverter<T> GetEnumConverter<T>(JsonSerializerOptions options
ThrowHelper.ThrowArgumentNullException(nameof(options));
}

return new EnumConverter<T>(EnumConverterOptions.AllowNumbers, options);
return EnumConverterFactory.Create<T>(EnumConverterOptions.AllowNumbers, options);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ public static IEnumerable<ITestData> GetTestDataCore()
yield return new TestData<IntEnum>(IntEnum.A, ExpectedJsonSchema: """{"type":"integer"}""");
yield return new TestData<StringEnum>(StringEnum.A, ExpectedJsonSchema: """{"enum":["A","B","C"]}""");
yield return new TestData<FlagsStringEnum>(FlagsStringEnum.A, ExpectedJsonSchema: """{"type":"string"}""");
yield return new TestData<EnumWithNameAttributes>(
EnumWithNameAttributes.Value1,
AdditionalValues: [EnumWithNameAttributes.Value2],
ExpectedJsonSchema: """{"enum":["A","B"]}""");

// Nullable<T> types
yield return new TestData<bool?>(true, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["boolean","null"]}""");
Expand Down Expand Up @@ -1077,6 +1081,15 @@ public enum StringEnum { A, B, C };
[Flags, JsonConverter(typeof(JsonStringEnumConverter<FlagsStringEnum>))]
public enum FlagsStringEnum { A = 1, B = 2, C = 4 };

[JsonConverter(typeof(JsonStringEnumConverter<EnumWithNameAttributes>))]
public enum EnumWithNameAttributes
{
[JsonStringEnumMemberName("A")]
Value1 = 1,
[JsonStringEnumMemberName("B")]
Value2 = 2,
}

public class SimplePoco
{
public string String { get; set; } = "default";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,16 @@ let ``Successful Deserialize Numeric label Of Enum When Allowing Integer Values`
let ``Successful Deserialize Numeric label Of Enum But as Underlying value When Allowing Integer Values`` () =
let actual = JsonSerializer.Deserialize<NumericLabelEnum>("\"3\"", options)
Assert.NotEqual(NumericLabelEnum.``3``, actual)
Assert.Equal(LanguagePrimitives.EnumOfValue 3, actual)
Assert.Equal(LanguagePrimitives.EnumOfValue 3, actual)

type CharEnum =
| A = 'A'
| B = 'B'
| C = 'C'

[<Fact>]
let ``Serializing char enums throws NotSupportedException`` () =
Assert.Throws<NotSupportedException>(fun () -> JsonSerializer.Serialize(CharEnum.A) |> ignore) |> ignore
Assert.Throws<NotSupportedException>(fun () -> JsonSerializer.Serialize(CharEnum.A, options) |> ignore) |> ignore
Assert.Throws<NotSupportedException>(fun () -> JsonSerializer.Deserialize<CharEnum>("0") |> ignore) |> ignore
Assert.Throws<NotSupportedException>(fun () -> JsonSerializer.Deserialize<CharEnum>("\"A\"", options) |> ignore) |> ignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public sealed partial class JsonSchemaExporterTests_SourceGen()
[JsonSerializable(typeof(IntEnum))]
[JsonSerializable(typeof(StringEnum))]
[JsonSerializable(typeof(FlagsStringEnum))]
[JsonSerializable(typeof(EnumWithNameAttributes))]
// Nullable<T> types
[JsonSerializable(typeof(bool?))]
[JsonSerializable(typeof(int?))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
Expand Down Expand Up @@ -832,5 +832,94 @@ private static JsonSerializerOptions CreateStringEnumOptionsForType<TEnum>(bool
{
return CreateStringEnumOptionsForType(typeof(TEnum), useGenericVariant, namingPolicy, allowIntegerValues);
}

[Theory]
[InlineData(EnumWithMemberAttributes.Value1, "CustomValue1")]
[InlineData(EnumWithMemberAttributes.Value2, "CustomValue2")]
[InlineData(EnumWithMemberAttributes.Value3, "Value3")]
public static void EnumWithMemberAttributes_StringEnumConverter_SerializesAsExpected(EnumWithMemberAttributes value, string expectedJson)
{
JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter() } };

string json = JsonSerializer.Serialize(value, options);
Assert.Equal($"\"{expectedJson}\"", json);
Assert.Equal(value, JsonSerializer.Deserialize<EnumWithMemberAttributes>(json, options));
}

[Theory]
[InlineData(EnumWithMemberAttributes.Value1)]
[InlineData(EnumWithMemberAttributes.Value2)]
[InlineData(EnumWithMemberAttributes.Value3)]
public static void EnumWithMemberAttributes_NoStringEnumConverter_SerializesAsNumber(EnumWithMemberAttributes value)
{
string json = JsonSerializer.Serialize(value);
Assert.Equal($"{(int)value}", json);
Assert.Equal(value, JsonSerializer.Deserialize<EnumWithMemberAttributes>(json));
}

[Theory]
[InlineData(EnumWithMemberAttributes.Value1, "CustomValue1")]
[InlineData(EnumWithMemberAttributes.Value2, "CustomValue2")]
[InlineData(EnumWithMemberAttributes.Value3, "value3")]
public static void EnumWithMemberAttributes_StringEnumConverterWithNamingPolicy_NotAppliedToCustomNames(EnumWithMemberAttributes value, string expectedJson)
{
JsonSerializerOptions options = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } };

string json = JsonSerializer.Serialize(value, options);
Assert.Equal($"\"{expectedJson}\"", json);
Assert.Equal(value, JsonSerializer.Deserialize<EnumWithMemberAttributes>(json, options));
}

public enum EnumWithMemberAttributes
{
[JsonStringEnumMemberName("CustomValue1")]
Value1 = 1,
[JsonStringEnumMemberName("CustomValue2")]
Value2 = 2,
Value3 = 3,
}

[Theory]
[InlineData(EnumFlagsWithMemberAttributes.Value1, "A")]
[InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2, "A, B")]
[InlineData(EnumFlagsWithMemberAttributes.Value1 | EnumFlagsWithMemberAttributes.Value2 | EnumFlagsWithMemberAttributes.Value3, "A, B, C")]
public static void EnumFlagsWithMemberAttributes_SerializesAsExpected(EnumFlagsWithMemberAttributes value, string expectedJson)
{
string json = JsonSerializer.Serialize(value);
Assert.Equal($"\"{expectedJson}\"", json);
Assert.Equal(value, JsonSerializer.Deserialize<EnumFlagsWithMemberAttributes>(json));
}

[Flags, JsonConverter(typeof(JsonStringEnumConverter<EnumFlagsWithMemberAttributes>))]
public enum EnumFlagsWithMemberAttributes
{
[JsonStringEnumMemberName("A")]
Value1 = 1,
[JsonStringEnumMemberName("B")]
Value2 = 2,
[JsonStringEnumMemberName("C")]
Value3 = 4,
}

[Theory]
[InlineData(EnumWithConflictingMemberAttributes.Value1)]
[InlineData(EnumWithConflictingMemberAttributes.Value2)]
[InlineData(EnumWithConflictingMemberAttributes.Value3)]
public static void EnumWithConflictingMemberAttributes_IsTolerated(EnumWithConflictingMemberAttributes value)
{
string json = JsonSerializer.Serialize(value);
Assert.Equal("\"Value3\"", json);
Assert.Equal(EnumWithConflictingMemberAttributes.Value1, JsonSerializer.Deserialize<EnumWithConflictingMemberAttributes>(json));
}

[JsonConverter(typeof(JsonStringEnumConverter<EnumWithConflictingMemberAttributes>))]
public enum EnumWithConflictingMemberAttributes
{
[JsonStringEnumMemberName("Value3")]
Value1 = 1,
[JsonStringEnumMemberName("Value3")]
Value2 = 2,
Value3 = 3,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SerializerTrimmingTest
{
Expand All @@ -16,12 +17,50 @@ internal class Program
static int Main(string[] args)
{
string json = JsonSerializer.Serialize(new ClassWithDay());
return json == @"{""Day"":0}" ? 100 : -1;
if (json != """{"Day":0}""")
{
return -1;
}

json = JsonSerializer.Serialize(new ClassWithDaySourceGen(), Context.Default.ClassWithDaySourceGen);
if (json != """{"Day":"Sun"}""")
{
return -2;
}

return 100;
}
}

internal class ClassWithDay
{
public DayOfWeek Day { get; set; }
}

internal class ClassWithDaySourceGen
{
[JsonConverter(typeof(JsonStringEnumConverter<DayOfWeek>))]
public DayOfWeek Day { get; set; }
}

internal enum DayOfWeek
{
[JsonStringEnumMemberName("Sun")]
Sunday,
[JsonStringEnumMemberName("Mon")]
Monday,
[JsonStringEnumMemberName("Tue")]
Tuesday,
[JsonStringEnumMemberName("Wed")]
Wednesday,
[JsonStringEnumMemberName("Thu")]
Thursday,
[JsonStringEnumMemberName("Fri")]
Friday,
[JsonStringEnumMemberName("Sat")]
Saturday
}

[JsonSerializable(typeof(ClassWithDaySourceGen))]
internal partial class Context : JsonSerializerContext;
}
Loading