Search code examples
c#system.text.json.net-7.0

Deserialize a class that implements IEnumerable


I develop a small program which allows to serialize/deserialize a class in Json.

This program, I make it .NET 7 and therefore I use System.Text.Json

So I have an ITest interface. This interface is implemented in two classes TestSuite and TestCase.

[JsonDerivedType(typeof(TestSuite), "testSuite")]
[JsonDerivedType(typeof(TestCase), "testCase")]
[JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]
public interface ITest
{
}
public class TestCase : ITest
{
    public string Name { get; set; }
}
public class TestSuite : ITest, IEnumerable<ITest>
{
    private readonly List<ITest> _tests = new ();
 
    public void Add(ITest test)
    {
        _tests.Add(test);
    }
 
    /// <inheritdoc />
    public IEnumerator<ITest> GetEnumerator()
    {
        return _tests.GetEnumerator();
    }
 
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

And to test, I do:

ITest suite = new TestSuite { new TestCase { Name = "Oui" }, new TestCase { Name = "Test2" } };
 
string json = JsonSerializer.Serialize(suite);
 
var s = JsonSerializer.Deserialize<ITest>(json);
 
Console.WriteLine(json);

TestCase serialization and deserialization works perfectly.

But for TestSuite deserialization fails with error message:

System.NotSupportedException: 'The collection type 'Test.TestSuite' is abstract, an interface, or is read only, and could not be instantiated and populated. Path: $.$values ​​| LineNumber: 0 | BytePositionInLine: 32.'

I can't use custom JsonConverter because json polymorphism only supports json converter by default.

Do you know how I could solve this problem?

Thanks in advance,

I tried to create a custom JsonConverter for TestSuite but I can't. Then I tried to abort json polymorphism and create a custom JsonConverter for ITest but this is not a good idea.


Solution

  • The problem here is that your class implements a collection interface and ATM there is no build in way to tell System.Text.Json to serialize your object as object, not as a collection (track this one for when it will become possible).

    If you really-really need to use current class structure you can go down a rabbit hole of reflection and mess with System.Text.Json internals (note that this can be very brittle):

    // tells to serialize `TestSuite` as object
    internal sealed class ObjectConverterFactory : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            return typeToConvert == typeof(TestSuite);
        }
    
        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            var type = typeof(JsonConverterFactory).Assembly.GetType("System.Text.Json.Serialization.Converters.ObjectConverterFactory");
            var f = (JsonConverterFactory)Activator.CreateInstance(type, new object[]{true});
            return  f.CreateConverter(typeToConvert, options);
        }
    }
    
    // exposes private property to serializer
    static void Test(JsonTypeInfo jsonTypeInfo)
    {
        if (jsonTypeInfo.Type == typeof(TestSuite))
        {
            var field = typeof(TestSuite).GetField("_tests", BindingFlags.Instance | BindingFlags.NonPublic);
            JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
            jsonPropertyInfo.Get = field.GetValue;
            jsonPropertyInfo.Set = field.SetValue;
            jsonTypeInfo.Properties.Add(jsonPropertyInfo);
        }
    }
    

    And usage:

    ITest suite = new TestSuite { new TestCase { Name = "Oui" }, new TestCase { Name = "Test2" } };
    
    var jsonSerializerOptions = new JsonSerializerOptions
    {
        Converters = { new ObjectConverterFactory() },
        TypeInfoResolver = new DefaultJsonTypeInfoResolver
        {
            Modifiers = { Test }
        }
    };
    string json1 = JsonSerializer.Serialize(suite, jsonSerializerOptions);
    
    var s = JsonSerializer.Deserialize<ITest>(json1, jsonSerializerOptions);
    

    Note again that this can be very brittle (mainly due to ObjectConverterFactory implementation) and I would recommend to to remove the IEnumerable<ITest> from TestSuite.