Polymorphic deserialization of Dictionary with Interface values always returns null using System.Text.Json

41 views Asked by At

I am trying to JSON serialize and deserialize the class PlanningData to a database. Serialization seems to work but when deserializing Dictionary<SomeEnum, MyInterface> always returns null even though I can correctly navigate the Dictionary keys and values in the Visual Studio JSON viewer when deserializing.

I am using Net 8 so I tried implementing this: System.Text.Json .NET 8 Polymorphic deserialization

and dbc's answer here: Is polymorphic deserialization possible in System.Text.Json?

but I could not get it to work, i.e. deserialization still returned null.

This is the code to set up serialization for the database:

            modelBuilder.Entity<SomeEntity>()
                .Property(c => c.PlanningData)
                .HasColumnName("PlanningData")
                .HasConversion(
                    v => JsonHelper.Serialize(v),
                    v => JsonHelper.Deserialize<PlanningData>(v)
                );
    public class PlanningData
    {
        public required ProfitLossData ProfitLossData { get; init; }
    }

    public class ProfitLossData
    {
// Deserialization of this property always returns null
        public Dictionary<string, List<ProfitLossDataLineItem>> ProfitLossDataLineItems { get; private set;}
    }
    public class ProfitLossDataLineItem : DataLineItem
    {
    }
public class DataLineItem
{
    [System.Text.Json.Serialization.JsonConverter(typeof(PlanningCalculatedResultConverter))]
    public Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption> AllPlanningCellValueObjects { get; set; } = new Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption>
                                        {
                                            { PlanningType.A, new Class1ThatImplementsInterface() },
                                            { PlanningType.B, new Class2ThatImplementsInterface() },
                                            { PlanningType.C, new Class3ThatImplementsInterface() },
                                            { PlanningType.D, new Class4ThatImplementsInterface() },
                                            { PlanningType.E, new Class5ThatImplementsInterface()},                        
    };
}
    public static class JsonHelper
    {
        static readonly JsonSerializerOptions _options = new()
        {
            Converters = { new PlanningCalculatedResultConverter(), new NullableObjectArrayConverter()  }
        };

        public static string Serialize<T>(T value)
        {
            return System.Text.Json.JsonSerializer.Serialize(value);
        }

        public static T Deserialize<T>(string json)
        {
// this never calls my PlanningCalculatedResultConverter
            var deserializedValue = System.Text.Json.JsonSerializer.Deserialize<T>(json, _options) ?? throw new InvalidOperationException("Deserialization returned null.");
            return deserializedValue;
        }
    }
public class PlanningCalculatedResultConverter : JsonConverter<Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption>>
{
    public override Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // Deserialize the JSON to a JsonDocument
        using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
        {
            var dictionary = new Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption>();

            // Iterate over each property in the JSON document
            foreach (var property in doc.RootElement.EnumerateObject())
            {
                // Parse the PlanningType from the property name
                if (Enum.TryParse<PlanningType>(property.Name, out var planningType))
                {
                    // Deserialize the value to the appropriate type
                    var value = DeserializePlanningCalculatedResult(property.Value, planningType, options);

                    // Add the deserialized value to the dictionary
                    dictionary.Add(planningType, value);
                }
                else
                {
                    throw new JsonException($"Unsupported PlanningType: {property.Name}");
                }
            }

            return dictionary;
        }
    }

    private IPlanningCalculatedResultAndRequiredInputDataForSingleOption DeserializePlanningCalculatedResult(JsonElement element, PlanningType planningType, JsonSerializerOptions options)
    {
        switch (planningType)
        {
            case PlanningType.A:
                return JsonSerializer.Deserialize<Class1ThatImplementsInterface>(element.GetRawText(), options);
            case PlanningType.B:
                return JsonSerializer.Deserialize<Class2ThatImplementsInterface>(element.GetRawText(), options);
            case PlanningType.C:
                return JsonSerializer.Deserialize<Class3ThatImplementsInterface>(element.GetRawText(), options);
            case PlanningType.D:
                return JsonSerializer.Deserialize<Class4ThatImplementsInterface>(element.GetRawText(), options);
            case PlanningType.E:
                return JsonSerializer.Deserialize<Class5ThatImplementsInterfaceatedResult>(element.GetRawText(), options);
            default:
                throw new JsonException($"Unsupported PlanningType: {planningType}");
        }
    }

    public override void Write(Utf8JsonWriter writer, Dictionary<PlanningType, IPlanningCalculatedResultAndRequiredInputDataForSingleOption> value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        foreach (var kvp in value)
        {
            writer.WritePropertyName(kvp.Key.ToString());

            JsonSerializer.Serialize(writer, kvp.Value, kvp.Value.GetType(), options);
        }
        writer.WriteEndObject();
    }
}
    public interface IPlanningCalculatedResultAndRequiredInputDataForSingleOption
    {
        public bool IsOptionInDefaultState => InputParameters.Values.All(x => x.IsParameterInDefaultState);
        public double?[] CalculatedOptionResult { get; set; }

        public PlanningType PlanningType { get; } 
        public Dictionary<InputParameter, ISingleInputParameter> InputParameters { get; }
    }

    public interface ISingleInputParameter
    {
        public bool IsParameterInDefaultState => InputValues.Length == 0;

        public double[] InputValues { get; set; }

        public string InputParameterName { get; set; }

        public bool IsPercentageData { get; set; }
    }
0

There are 0 answers