3

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
SuperYUKO
  • 35
  • 3
  • I think you have an abstraction problem, `TestSuite` should not be an `ITest`, rather, an `ITestCollection` because a collection of tests is simply not a test. – aybe Nov 30 '22 at 02:34
  • I have no choice, TestSuite must implement ITest for the rest of the program and I must follow a given architecture – SuperYUKO Nov 30 '22 at 03:07
  • Do you really need `TestSuite` to implement `IEnumerable`? – Guru Stron Nov 30 '22 at 06:32

1 Answers1

1

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132