42

I will preface this by saying that I know what the problem is, I just don't know how to solve it. I am communicating with a .NET SOA data layer that returns data as JSON. One such method returns an object that has several collections within it. The object basically looks like this:

{
  "Name":"foo",
  "widgetCollection":[{"name","foo"}, {"name","foo"},],
  "cogCollection": [{"name","foo"}, {"childCogs",<<new collection>>},],
}

My class that represents this object looks like this:

public class SuperWidget : IWidget
{
    public string Name { get; set; }

    public ICollection<IWidget> WidgetCollection { get; set; }
    public ICollection<ICog> CogCollection { get; set; }

    public SuperWidget()
    {
    }

    [JsonConstructor]
    public SuperWidget(IEnumerable<Widget> widgets, IEnumerable<Cog> cogs)
    {
        WidgetCollection = new Collection<IWidget>();
        CogCollection = new Collection<ICog>();

        foreach (var w in widgets)
        {
            WidgetCollection.Add(w);
        }
        foreach (var c in cogs)
        {
            CogCollection.Add(c);
        }
    }
}

This constructor worked fine until the cogCollection added a child collection, and now I am getting the above error. A concrete cog class looks like this:

[Serializable]
public class Cog : ICog
{
    public string name { get; set; }

    public ICollection<ICog> childCogs { get; set; }        
}  

I don't want to change the collection to a concrete type because I am using IoC. Because I am using IoC I would really like to get away from the need to have the JsonConstructors that take concrete parameters, but I haven't figured out a way to do that. Any advice would be greatly appreciated!

Update:

Yuval Itzchakov's suggestion that this question is probably a duplicate is somewhat true (it seems). In the post referenced, one of the answers down the page provides same solution that was provided here. I didn't notice that answer, since the OP's question was different then the one I had here. My mistake.

-------my solution--------

As I said: Matt's solution took a little bit of work but I got something setup that works for me. The one thing I didn't like about his initial solution, were lines like these:

 return objectType == typeof(ICog); 

Following this pattern you would need to have a JsonConverter for every abstract type that you receive over the wire. This is less than ideal in my situation, so I created a generic JsonConverter as such:

public class GenericJsonConverter<T>: JsonConverter, IBaseJsonConverter<T>
{
    private readonly IUnityContainer Container;
    public GenericJsonConverter(IUnityContainer container)
    {
        Container = container;
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var target = serializer.Deserialize<Newtonsoft.Json.Linq.JObject>(reader);
        var result = Container.Resolve<T>();
        serializer.Populate(target.CreateReader(), result);
        return result;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

Then just before I deserialize my data, I do something like this:

 var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
 settings.Converters.Add((JsonConverter)Container.Resolve<IBaseJsonConverter<ICog>>());

Protip: If you use Resharper, (JsonConverter) will give you a suspicious cast warning in this scenario.

Hopefully someone else finds this useful down the road!

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
dparsons
  • 2,812
  • 5
  • 28
  • 44
  • Try using `TypeNameHandling.All` of the `JsonSerializerSettings` class and passing it to the json serializer – Yuval Itzchakov Jul 09 '14 at 03:18
  • 1
    possible duplicate of [Casting interfaces for deserialization in JSON.NET](http://stackoverflow.com/questions/5780888/casting-interfaces-for-deserialization-in-json-net) – Yuval Itzchakov Jul 09 '14 at 03:26
  • possible duplicate of [Using Json.NET converters to deserialize properties](http://stackoverflow.com/questions/2254872/using-json-net-converters-to-deserialize-properties) – nawfal Jul 20 '14 at 11:58
  • You can add a solution to your own question instead of editing the text of the question with answer text. – HackSlash Apr 07 '23 at 22:21

1 Answers1

43

You'll need to provide a custom serializer to Json.Net to tell it how to handle the child cogs. For example:

var settings = new JsonSerializerSettings();
settings.Converters.Add(new CogConverter());

Your CogConverter will need to inherit from JsonConverter and specify that it CanConvert your ICog interface. Perhaps something along the lines of:

public class CogConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(ICog);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return serializer.Deserialize(reader, typeof(Cog));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

I'd recommend registering your JsonSerializerSettings with your IoC container in that case. You may want to consider giving CogConverter access to the container, too, if the serializer can't be responsible for actually constructing the Cog itself; that all depends on your particular architecture.

Edit

Upon further reading, it seems like you might be looking for specifically how to use the IoC created ICog for population. I'm using the following as part of my ReadJson:

var target = serializer.Deserialize<Newtonsoft.Json.Linq.JObject>(reader);
var objectType = DetermineConcreteType(target);
var result = iocContainer.Resolve(objectType);
serializer.Populate(target.CreateReader(), result);
return result;

This allows you to use ANY object and populate it from the original JSON, using custom types as you wish inside your DetermineConcreteType method.

Matt DeKrey
  • 11,582
  • 5
  • 54
  • 69
  • With a little bit of work, this was the correct answer. I am curious though, what does your 'DetermineConcreteType' method do? – dparsons Jul 10 '14 at 02:30
  • In ours, we look at a few properties on it to determine a correct subtype (our interfaces aren't 1:1 with implementations) - a generic one I can think of would be a drop-down indicating payment method (credit card vs. bank, etc.) - each has different fields, but the drop down's value can indicate that type. If you have code that you want to share as part of the answer, I'd happily update mine to include a more complete solution. – Matt DeKrey Jul 10 '14 at 11:40
  • Cool, thanks for sharing! I'd recommend using a DI'd `JsonSerializerSettings` rather than building it directly before; you'd be able to test the `JsonSerializerSettings` registration with your container to ensure it had the appropriate converters in it, not have to worry about the suspicious cast, and still be able to test the converter with the generic independently. – Matt DeKrey Jul 11 '14 at 02:41
  • @MattDeKrey This works for me with only one object(e.g. Cog), how can I get it to work with a different Type of `ICog` e.g `DifferentCog` at the same time which has different properties from `Cog`. e.g. json "Icogs":[{"CogName":"name","CogOtherProperty":"prop"},{"DifferentCogAge":"ag11","DifferentCogOtherOtherProperty":"prop2"}]` – Dev Jun 17 '20 at 19:21