Search code examples
c#.netmodel-view-controller

C# - Post a model with generic property (Web API)


I have this situation:

public class Dto 
{
    public int TypeId { get; set; }
    public IType Type { get; set; }
}

public class Type1 : IType
{
   public string PropertyA { get; set; }
}

public class Type2 : IType
{
   public int PropertyB { get; set; }
   public bool PropertyC { get; set; }
}

public class MyController : ApiController
{
   [HttpPost]
   public IHttpActionResult Post(Dto dto) 
   {
   }
}

How can I deserialize for the correct implementation of IType interface, depending on the value of the TypeId property?

I tried using JsonConverter (following this example: https://gist.github.com/teamaton/bba69cf95b9e6766f231), but I can only specify one concrete type:

public class Dto 
{
    public int TypeId { get; set; }

    [JsonConverter(typeof(ConcreteTypeConverter<Type1>)]
    public IType Type { get; set; }
}

Solution

  • JsonConverter is the correct way to go, however the ConcreteTypeConverter isn't for your case.

    Assume you need to determine which concrete type to create at runtime based on TypeId, you will need a JsonConverter on Dto not on Type property.

    Try this:

    [JsonConverter(typeof(DtoJsonConverter))]
    public class Dto
    {
        public IType Type { get; set; }
        public int TypeId { get; set; }
    }
    
    class DtoJsonConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(Dto);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
            {
                return null;
            }
    
            // Load this to a JObject so that we can read TypeId
            var obj = JObject.Load(reader);
            var typeId = obj["TypeId"].Value<int>();
    
            // Figure out JSON covnerter for type property based on TypeId
            JsonConverter converter = null;
            switch (typeId)
            {
                // Assuming 1 means Type1
                case 1:
                    converter = new CreateITypeJsonConverter(() => new Type1());
                    break;
                case 2:
                    converter = new CreateITypeJsonConverter(() => new Type2());
                    break;
            }
    
            if (converter != null)
            {
                serializer.Converters.Add(converter);
            }
    
            try
            {
                // Now create Dto and populate the object.
                // This will call the JsonConverter we just added for Type property.
                var dto = new Dto();
                serializer.Populate(obj.CreateReader(), dto);
                return dto;
            }
            finally
            {
                if (converter != null)
                {
                    serializer.Converters.Remove(converter);
                }
            }
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotSupportedException();
        }
    }
    
    class CreateITypeJsonConverter : CustomCreationConverter<IType>
    {
        private readonly Func<IType> _factory;
    
        public CreateITypeJsonConverter(Func<IType> factory)
        {
            _factory = factory;
        }
    
        public override IType Create(Type objectType)
        {
            return _factory();
        }
    }
    

    The DtoJsonConverter works out the concrete type of IType as per the value of TypeId, and use another CreateITypeJsonConverter to instantiate the concrete type, then populate the Dto.

    It is also possible that you could move TypeId into IType, then use one of the methods in this question: JsonConverter with Interface