Search code examples
c#asp.netjson.netdeserialization

Deserialization of object with properties of interface type in c#


I have a Square and Side element both implementing respective interfaces ISquare and ISide. Square has a Side property of type ISide. Side has a property int Len.

[JsonConverter(typeof(ConcreteTypeConverter<Square>))]
public interface ISquare
{
    ISide Side { get; }
}

internal class Square : ISquare
{
    public ISide Side { get; }

    [JsonConstructor]
    internal Square(Side side)
    {
        Side = side;
    }
}

[JsonConverter(typeof(ConcreteTypeConverter<Side>))]
public interface ISide
{
    int Len { get; }
}

internal class Side : ISide
{
    public int Len { get; }

    public Side(int len)
    {
        Len = len;
    }
}

This is the converter that I'm using for deserialization:

public class ConcreteTypeConverter<TConcrete> : JsonConverter<TConcrete>
{
    public override bool CanConvert(Type objectType)
    {
        //Assume we can convert to anything. It's just an example.
        return true;
    }
    public override TConcrete? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        //Here I explicitly specify the concrete type I want to create
        return JsonSerializer.Deserialize<TConcrete>(ref reader, options);
    }

    public override void Write(Utf8JsonWriter writer, TConcrete value, JsonSerializerOptions options)
    {
        //Just fine
        JsonSerializer.Serialize(writer, value, options);
    }
}

The problem is that when I try to call the following api end point:

[HttpPost(Name = "side-len")]
public int Post([FromBody] ISquare square)
{
    return square.Side.Len;
}

With json:

{
  "side": {
    "Len": 5
  }
}

I get the following error:

System.InvalidOperationException: 'Each parameter in the deserialization constructor on type 'ProvaModel.Square' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled. The match can be case-insensitive.'

I know that the problem is in the property of Square class being of type ISide, if I recreate the same example using the concrete class Side it works.

I understand this is mapping that can be solved using concrete classes, but I really need to keep properties as interface types. Is there any way to tell the converter that the property ISide Side should be converted using Side concrete class?


Solution

  • My guess is that your converter produces a ISide-object for the property, but there is no constructor on Square that takes such a type. So I would test changing the constructor to take a ISide.

    But you should ask yourself what value the interface adds if you are not providing multiple implementations. For simple types like "square" I would likely recommend just using concrete types. You say you need to use interfaces, but not why, and that might indicate an XY-problem.

    In most cases where interfaces are used you would use polymorphic serialization. This works by adding type information to the serialized data, allowing the correct type to be deserialized.

    If your types are more complex you may consider creating separate types just for serialization. These are sometimes called "Data Transfer Object" (DTOs). This will involve a bit of extra code, but it can help separating the concerns of storing data from any logic that might involve temporary or intermediate values. Something like AutoMapper may help with the DTO <-> domain object conversion. DTOs can also improve compatibility since some libraries can have problems figuring out how to construct objects, a public parameterles constructor and public setters for all properties tend to provide the best compatibility.