-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Comments
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis Issue DetailsThere'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 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:
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 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 [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.
|
Duplicate of #54189. Workarounds include annoting the enum definition with |
@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. |
That's not quite right, registering a custom converter for type |
If you want your app to support arbitrary enum types, you might also want consider putting your custom converter implementation behind a |
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:
And you can see that this works fine with the following unit test:
However, if I'm looking to serialize a list of enums as in the following, I receive an exception (shared under the unit test):
This throws the following exception:
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:And with a small tweak to the unit test to refer specifically to that type, it will work:
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 aJsonStringEnumConverter
or aJsonStringEnumListConverter
, 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:And the unit tests demonstrating that it works:
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.
The text was updated successfully, but these errors were encountered: