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

Transparently serialize collections with custom JsonConverter #85479

Closed
WhitWaldo opened this issue Apr 27, 2023 · 5 comments
Closed

Transparently serialize collections with custom JsonConverter #85479

WhitWaldo opened this issue Apr 27, 2023 · 5 comments

Comments

@WhitWaldo
Copy link

There's a list of collection types that are supported in System.Text.Json that's quite extensive. However, these collection types only appear to be supported if the remainder of the serialization operation can be performed with built-in converters.

As I'm seeking to serialize Enums using the value from a [EnumMember] attribute and this isn't supported, per this since-closed issue, I'm using a custom JsonSerializer shared by someone on that thread.

Copying the JsonConverter over here, this looks like the following:

public sealed class JsonStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
    private readonly Dictionary<TEnum, string> _enumToString = new();
    private readonly Dictionary<string, TEnum> _stringToEnum = new();

    /// <inheritdoc />
    public JsonStringEnumConverter()
    {
        var type = typeof(TEnum);
        var values = Enum.GetValues<TEnum>();

        foreach (var value in values)
        {
            var enumMember = type.GetMember(value.ToString())[0];
            var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                .Cast<EnumMemberAttribute>()
                .FirstOrDefault();

            _stringToEnum.Add(value.ToString(), value);

            if (attr?.Value != null)
            {
                _enumToString.Add(value, attr.Value);
                _stringToEnum.Add(attr.Value, value);
            }
            else
            {
                _enumToString.Add(value, value.ToString());
            }
        }
    }

    /// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    /// <returns>The converted value.</returns>
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var stringValue = reader.GetString();

        if (stringValue != null && _stringToEnum.TryGetValue(stringValue, out var enumValue))
        {
            return enumValue;
        }

        return default;
    }

    /// <summary>Writes a specified value as JSON.</summary>
    /// <param name="writer">The writer to write to.</param>
    /// <param name="value">The value to convert to JSON.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(_enumToString[value]);
    }
}

And you can see that this works fine with the following unit test:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeWithEnumMemberValue() //Works fine
    {
        var obj = new MyTestObj(Colors.Red);
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":""r""}", sut);
    }

    private record MyTestObj([property:JsonPropertyName("color"),JsonConverter(typeof(JsonStringEnumConverter<Colors>))]Colors Color);
    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

However, if I'm looking to serialize a list of enums as in the following, I receive an exception (shared under the unit test):

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeListWithEnumMemberValues() //Throws exception below
    {
        var obj = new ManyTestObj(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record ManyTestObj(
        [property: JsonPropertyName("color"), JsonConverter(typeof(JsonStringEnumConverter<Colors>))]
        List<Colors> Colors);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

This throws the following exception:

System.InvalidOperationException: The converter specified on 'xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests+ManyTestObj.Colors' is not compatible with the type 'System.Collections.Generic.List1[xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests+Colors]'. at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(Type classTypeAttributeIsOn, MemberInfo memberInfo, Type typeToConvert) at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options) at System.Text.Json.Serialization.Metadata.ReflectionJsonTypeInfo1.CreateProperty(Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options, Boolean shouldCheckForRequiredKeyword) at System.Text.Json.Serialization.Metadata.ReflectionJsonTypeInfo`1.LateAddProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializePropertyCache() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureLocked|143_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable) at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType) at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options) at xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests.ShouldSerializeListWithEnumMemberValues() in xyz.Tests\Serialization\Json\Converters\JsonStringEnumConverterTests.cs:line 24 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

Newtsonsoft.Json's version of a JsonConverter is smart enough to figure out when a collection type is presented and transparently use the JsonConverter for each member of that collection, but not blindly pass the whole collection into the JsonConvert itself. This works without a hitch using that serializer, but I'm trying to use System.Text.Json through and through so I don't wind up having multiple serializers in place for different use-cases.

Now, I can easily just have a second JsonConverter that handles a List<TEnum> explicitly:

public sealed class JsonStringEnumListConverter<TEnum> : JsonConverter<List<TEnum>> where TEnum : struct, Enum
{
    private readonly Dictionary<TEnum, string> _enumToString = new();
    private readonly Dictionary<string, TEnum> _stringToEnum = new();

    /// <inheritdoc />
    public JsonStringEnumListConverter()
    {
        var type = typeof(TEnum);
        var values = Enum.GetValues<TEnum>();

        foreach (var value in values)
        {
            var enumMember = type.GetMember(value.ToString())[0];
            var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                .Cast<EnumMemberAttribute>()
                .FirstOrDefault();

            _stringToEnum.Add(value.ToString(), value);

            if (attr?.Value != null)
            {
                _enumToString.Add(value, attr.Value);
                _stringToEnum.Add(attr.Value, value);
            }
            else
            {
                _enumToString.Add(value, value.ToString());
            }
        }
    }

    /// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    /// <returns>The converted value.</returns>
    public override List<TEnum>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
        {
            throw new JsonException();
        }

        reader.Read();

        var elements = new List<TEnum>();
        while (reader.TokenType != JsonTokenType.EndArray)
        {
            var stringValue = reader.GetString();
            if (stringValue != null && _stringToEnum.TryGetValue(stringValue, out var enumValue))
            {
                elements.Add(enumValue);
            }
            reader.Read();
        }

        return elements;
    }
    
    /// <summary>Writes a specified value as JSON.</summary>
    /// <param name="writer">The writer to write to.</param>
    /// <param name="value">The value to convert to JSON.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    public override void Write(Utf8JsonWriter writer, List<TEnum> value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        foreach (var item in value)
        {
            writer.WriteStringValue(_enumToString[item]);
        }

        writer.WriteEndArray();
    }
}

And with a small tweak to the unit test to refer specifically to that type, it will work:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeListWithEnumMemberValues() //Works fine
    {
        var obj = new ManyTestObj(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record ManyTestObj(
        [property: JsonPropertyName("color"), JsonConverter(typeof(JsonStringEnumListConverter<Colors>))]
        List<Colors> Colors);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

But this means that for every kind of collection I want to serialize, I'm going to have to create a distinct concrete implementation of the JsonConverter around my generic enum and also remember to cite the appropriate one in the attribute.

Now, this can be alleviated somewhat through use of a custom JsonConverterAttribute in that I can decorate the intended property with the attribute throughout and lean on the mapping hidden in this implementation to properly map to either a JsonStringEnumConverter or a JsonStringEnumListConverter, but again, that doesn't quite get at the issue I'd like to solve here. For transparency, this is what I'm using here:

[AttributeUsage(AttributeTargets.Property)]
public class JsonStringEnumAttribute<TEnum> : JsonConverterAttribute where TEnum : struct, Enum
{
    /// <summary>
    /// Creates a JsonConverter based on the type provided.
    /// </summary>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <returns></returns>
    public override JsonConverter? CreateConverter(Type typeToConvert)
    {
        if (typeToConvert == typeof(TEnum))
        {
            return new JsonStringEnumConverter<TEnum>();
        }

        if (typeToConvert == typeof(List<TEnum>))
        {
            return new JsonStringEnumListConverter<TEnum>();
        }

        throw new ArgumentException(
            $"This converter only works with enum and List<enum> types and it was provided {typeToConvert}");
    }
}

And the unit tests demonstrating that it works:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void SingleEntityAttributeTest() //Works
    {
        var obj = new SingleAttributeTestObj(Colors.Red);
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":""r""}", sut);
    }

    [TestMethod]
    public void ListEntityAttributeTest() //Works
    {
        var obj = new MultipleAttributeTestObject(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record SingleAttributeTestObj([property: JsonPropertyName("color"), JsonStringEnum<Colors>]
        Colors color);
    private record MultipleAttributeTestObject([property: JsonPropertyName("color"), JsonStringEnum<Colors>]
        List<Colors> color);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

Ideally, I would like instead to be able to write only the first JsonConverter presented in this issue and trust that when System.Text.Json claims it supports (de)serialization of collections, that it also handles the serialization to/from the collection of the type when using a custom converter as well as any built-in converters.

Thank you for your consideration.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Apr 27, 2023
@ghost
Copy link

ghost commented Apr 27, 2023

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

There's a list of collection types that are supported in System.Text.Json that's quite extensive. However, these collection types only appear to be supported if the remainder of the serialization operation can be performed with built-in converters.

As I'm seeking to serialize Enums using the value from a [EnumMember] attribute and this isn't supported, per this since-closed issue, I'm using a custom JsonSerializer shared by someone on that thread.

Copying the JsonConverter over here, this looks like the following:

public sealed class JsonStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
    private readonly Dictionary<TEnum, string> _enumToString = new();
    private readonly Dictionary<string, TEnum> _stringToEnum = new();

    /// <inheritdoc />
    public JsonStringEnumConverter()
    {
        var type = typeof(TEnum);
        var values = Enum.GetValues<TEnum>();

        foreach (var value in values)
        {
            var enumMember = type.GetMember(value.ToString())[0];
            var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                .Cast<EnumMemberAttribute>()
                .FirstOrDefault();

            _stringToEnum.Add(value.ToString(), value);

            if (attr?.Value != null)
            {
                _enumToString.Add(value, attr.Value);
                _stringToEnum.Add(attr.Value, value);
            }
            else
            {
                _enumToString.Add(value, value.ToString());
            }
        }
    }

    /// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    /// <returns>The converted value.</returns>
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var stringValue = reader.GetString();

        if (stringValue != null && _stringToEnum.TryGetValue(stringValue, out var enumValue))
        {
            return enumValue;
        }

        return default;
    }

    /// <summary>Writes a specified value as JSON.</summary>
    /// <param name="writer">The writer to write to.</param>
    /// <param name="value">The value to convert to JSON.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(_enumToString[value]);
    }
}

And you can see that this works fine with the following unit test:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeWithEnumMemberValue() //Works fine
    {
        var obj = new MyTestObj(Colors.Red);
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":""r""}", sut);
    }

    private record MyTestObj([property:JsonPropertyName("color"),JsonConverter(typeof(JsonStringEnumConverter<Colors>))]Colors Color);
    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

However, if I'm looking to serialize a list of enums as in the following, I receive an exception (shared under the unit test):

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeListWithEnumMemberValues() //Throws exception below
    {
        var obj = new ManyTestObj(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record ManyTestObj(
        [property: JsonPropertyName("color"), JsonConverter(typeof(JsonStringEnumConverter<Colors>))]
        List<Colors> Colors);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

This throws the following exception:

System.InvalidOperationException: The converter specified on 'xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests+ManyTestObj.Colors' is not compatible with the type 'System.Collections.Generic.List1[xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests+Colors]'. at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializationConverterOnAttributeNotCompatible(Type classTypeAttributeIsOn, MemberInfo memberInfo, Type typeToConvert) at System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.GetConverterFromAttribute(JsonConverterAttribute converterAttribute, Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options) at System.Text.Json.Serialization.Metadata.ReflectionJsonTypeInfo1.CreateProperty(Type typeToConvert, MemberInfo memberInfo, JsonSerializerOptions options, Boolean shouldCheckForRequiredKeyword) at System.Text.Json.Serialization.Metadata.ReflectionJsonTypeInfo`1.LateAddProperties() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializePropertyCache() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure() at System.Text.Json.Serialization.Metadata.JsonTypeInfo.g__ConfigureLocked|143_0() at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable) at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType) at System.Text.Json.JsonSerializer.GetTypeInfo[T](JsonSerializerOptions options) at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options) at xyz.Tests.Serialization.Json.Converters.JsonStringEnumConverterTests.ShouldSerializeListWithEnumMemberValues() in xyz.Tests\Serialization\Json\Converters\JsonStringEnumConverterTests.cs:line 24 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

Newtsonsoft.Json's version of a JsonConverter is smart enough to figure out when a collection type is presented and transparently use the JsonConverter for each member of that collection, but not blindly pass the whole collection into the JsonConvert itself. This works without a hitch using that serializer, but I'm trying to use System.Text.Json through and through so I don't wind up having multiple serializers in place for different use-cases.

Now, I can easily just have a second JsonConverter that handles a List<TEnum> explicitly:

public sealed class JsonStringEnumListConverter<TEnum> : JsonConverter<List<TEnum>> where TEnum : struct, Enum
{
    private readonly Dictionary<TEnum, string> _enumToString = new();
    private readonly Dictionary<string, TEnum> _stringToEnum = new();

    /// <inheritdoc />
    public JsonStringEnumListConverter()
    {
        var type = typeof(TEnum);
        var values = Enum.GetValues<TEnum>();

        foreach (var value in values)
        {
            var enumMember = type.GetMember(value.ToString())[0];
            var attr = enumMember.GetCustomAttributes(typeof(EnumMemberAttribute), false)
                .Cast<EnumMemberAttribute>()
                .FirstOrDefault();

            _stringToEnum.Add(value.ToString(), value);

            if (attr?.Value != null)
            {
                _enumToString.Add(value, attr.Value);
                _stringToEnum.Add(attr.Value, value);
            }
            else
            {
                _enumToString.Add(value, value.ToString());
            }
        }
    }

    /// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
    /// <param name="reader">The reader.</param>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    /// <returns>The converted value.</returns>
    public override List<TEnum>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartArray)
        {
            throw new JsonException();
        }

        reader.Read();

        var elements = new List<TEnum>();
        while (reader.TokenType != JsonTokenType.EndArray)
        {
            var stringValue = reader.GetString();
            if (stringValue != null && _stringToEnum.TryGetValue(stringValue, out var enumValue))
            {
                elements.Add(enumValue);
            }
            reader.Read();
        }

        return elements;
    }
    
    /// <summary>Writes a specified value as JSON.</summary>
    /// <param name="writer">The writer to write to.</param>
    /// <param name="value">The value to convert to JSON.</param>
    /// <param name="options">An object that specifies serialization options to use.</param>
    public override void Write(Utf8JsonWriter writer, List<TEnum> value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        foreach (var item in value)
        {
            writer.WriteStringValue(_enumToString[item]);
        }

        writer.WriteEndArray();
    }
}

And with a small tweak to the unit test to refer specifically to that type, it will work:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void ShouldSerializeListWithEnumMemberValues() //Works fine
    {
        var obj = new ManyTestObj(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record ManyTestObj(
        [property: JsonPropertyName("color"), JsonConverter(typeof(JsonStringEnumListConverter<Colors>))]
        List<Colors> Colors);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

But this means that for every kind of collection I want to serialize, I'm going to have to create a distinct concrete implementation of the JsonConverter around my generic enum and also remember to cite the appropriate one in the attribute.

Now, this can be alleviated somewhat through use of a custom JsonConverterAttribute in that I can decorate the intended property with the attribute throughout and lean on the mapping hidden in this implementation to properly map to either a JsonStringEnumConverter or a JsonStringEnumListConverter, but again, that doesn't quite get at the issue I'd like to solve here. For transparency, this is what I'm using here:

[AttributeUsage(AttributeTargets.Property)]
public class JsonStringEnumAttribute<TEnum> : JsonConverterAttribute where TEnum : struct, Enum
{
    /// <summary>
    /// Creates a JsonConverter based on the type provided.
    /// </summary>
    /// <param name="typeToConvert">The type to convert.</param>
    /// <returns></returns>
    public override JsonConverter? CreateConverter(Type typeToConvert)
    {
        if (typeToConvert == typeof(TEnum))
        {
            return new JsonStringEnumConverter<TEnum>();
        }

        if (typeToConvert == typeof(List<TEnum>))
        {
            return new JsonStringEnumListConverter<TEnum>();
        }

        throw new ArgumentException(
            $"This converter only works with enum and List<enum> types and it was provided {typeToConvert}");
    }
}

And the unit tests demonstrating that it works:

[TestClass]
public sealed class JsonStringEnumConverterTests
{
    [TestMethod]
    public void SingleEntityAttributeTest() //Works
    {
        var obj = new SingleAttributeTestObj(Colors.Red);
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":""r""}", sut);
    }

    [TestMethod]
    public void ListEntityAttributeTest() //Works
    {
        var obj = new MultipleAttributeTestObject(new List<Colors> {Colors.Red, Colors.Blue});
        var sut = JsonSerializer.Serialize(obj);

        Assert.AreEqual(@"{""color"":[""r"",""b""]}", sut);
    }

    private record SingleAttributeTestObj([property: JsonPropertyName("color"), JsonStringEnum<Colors>]
        Colors color);
    private record MultipleAttributeTestObject([property: JsonPropertyName("color"), JsonStringEnum<Colors>]
        List<Colors> color);

    private enum Colors
    {
        [EnumMember(Value="r")]
        Red,
        [EnumMember(Value="b")]
        Blue,
        [EnumMember(Value="g")]
        Green
    }
}

Ideally, I would like instead to be able to write only the first JsonConverter presented in this issue and trust that when System.Text.Json claims it supports (de)serialization of collections, that it also handles the serialization to/from the collection of the type when using a custom converter as well as any built-in converters.

Thank you for your consideration.

Author: WhitWaldo
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@eiriktsarpalis
Copy link
Member

Duplicate of #54189. Workarounds include annoting the enum definition with JsonConverterAttribute or registering your converter with the JsonSerializerOptions.Converters list.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Apr 28, 2023
@WhitWaldo
Copy link
Author

@eiriktsarpalis While I agree this is a duplicate, I disagree that those are viable workarounds as I still need a concrete implementation of every permutation of a converter to register either in the attribute or the serializer options.

@WhitWaldo WhitWaldo changed the title Transparently serialize collections with custom JSonConverter Transparently serialize collections with custom JsonConverter Apr 28, 2023
@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Apr 28, 2023

I still need a concrete implementation of every permutation of a converter

That's not quite right, registering a custom converter for type Foo using the approaches suggested should result in the custom converter working for List<Foo>, Dictionary<string, Foo> or any POCO containing a property of type Foo as well without any additional configuration.

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Apr 28, 2023

If you want your app to support arbitrary enum types, you might also want consider putting your custom converter implementation behind a JsonConverterFactory:

https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0#sample-factory-pattern-converter

@ghost ghost locked as resolved and limited conversation to collaborators May 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants