2

I'm calling a .NET Web Api from an Angular client and I use JSON to serialize / deserialize data.

I'm trying to deserialize a Model object which contains a list of Detector. Detector is an abstract class with two inherited childs: TextDetector and ColorDetector.

To be able to recognize which type of Detector need to be instanciated during deserialization, I've set TypeNameHandling.Auto to my JsonSerializerSettings (.NET side).

Typical get response for a Model containing a TextDetector

{
  "Detectors": {
    "$type": "System.Collections.Generic.HashSet`1[[I2D_Api.Models.Detector, I2D Api]], System.Core",
    "$values": [
      {
        "$type": "I2D_Api.Models.TextDetector, I2D Api",
        "Id": 1,
        "Name": "Detector0",
        "Bounds": "0, 0, 0, 0"
      }
    ]
  },
  "Id": 1,
  "Name": "Modèle 0",
  "ImgModelLink": "https://gamewave.fr/static/images/medias/upload/Fortnite/canards/Fortnite_20180515121231.jpg"
}

But I don't like this solution (which I can't even make work) because this would suggest that I have to MANUALLY typed my data on each client side request (Post or Put) to be correctly deserialize .NET side... In addition to that, I have not succeed yet in deserialize this JSON into a Model object client side because of the $ prefixes (but this is faisible i guess)

I think i'm going the wrong way, I've search but can't find another way to pass this collection, and this seems to be way to dirty. Is there another way to do this ?


Note: i'm using EntityFramework, this is my get method

[ResponseType(typeof(Model))]
public IHttpActionResult GetModel(int id)
{
    Model model = db.Models.Find(id);
    if (model == null)
    {
        return NotFound();
    }

    return Ok(model);
}

EDIT: So I followed @dbc link : Deserializing polymorphic json classes without type information which is EXACTLY what I want. I implemented a Json converter, which know child class to instanciate based on missing property Color (no color property = TextDetector, color property = ColorDetector).

Also replaced my Bounds property (JsonProperty.ValueProvider was unable to extract value on struct) by X, Y, Width and Height properties in Detector class (not an issue). Finally changed TypeNameHandling to Object.

But now I got a stackoverflow exception which I don't understand. Notice that TextDetector has no more attributes / properties than Detector Errors comes from this line in my DetectorConverter

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    JObject item = JObject.Load(reader); // This throw stackoverflow exception
    if (item["Color"] == null)
    {
        return item.ToObject<TextDetector>();
    }
    else
    {
        return item.ToObject<ColorDetector>();
    }
}

On first ReadJson execution, no exception. But then, it look like JObject.load(reader) causing a loop internally which i can't see in the stack beaucause it's external code. Only thing I know is that item type after loading from reader is Detector at first execution and then TextDetector, looping forever..

  • 1
    Deleted my comment. I see your problem now, good question. –  May 17 '18 at 16:00
  • Just as an aside, I'm not sure 'manually' specifying the type is a bad or hacky approach. –  May 17 '18 at 16:02
  • 2
    Couple of ideas: 1) Split the Detectors array into to, TextDetectors and ColorDetectors. 2) Don't rely on .Net's built-in $type property but add your own Type property. – Christoph May 17 '18 at 16:03
  • Do you ever have send a Model object from the client to the server? I.e. does the server ever have to figure out, which type of object it just received? If you are updating detectors individually (as opposed to an entire model) you could have two different endpoints, and for each endpoint accept a different type. – Christoph May 17 '18 at 16:04
  • 1
    A few suggestions: 1) Use `TypeNameHandling` only for objects and not for collections. While the JSON payload may specify the object types, the receiving code should always be free to choose a different collection type. (E.g. you might want to replace that `HashSet` with a `List` someday. 2) Possibly the approach from [Deserializing polymorphic json classes without type information using json.net](https://stackoverflow.com/a/19308474/3744182) for you. – dbc May 17 '18 at 17:17
  • 2
    3) You could create a [custom serialization binder](https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm) that uses predefined names for the expected polymorphic types, freeing your sending system from having to know actual c# type name information. See also [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954/3744182) for why this is a good idea from a security perspective. – dbc May 17 '18 at 17:19
  • Wow thanks guys, lot of good stuff here. I'd like to keep only one detector array for simplicity. I've think about using my own type property but wanted to see if there was other way :). TypeNameHandling only for objects might be an easy solve ! I'm getting on it and gives feed back then. Thanks you all guys ! – Sebastien Servouze May 18 '18 at 07:33
  • @dbc Thanks, I went your way, but i'm facing a last small issue, I edited my post – Sebastien Servouze May 18 '18 at 11:26

1 Answers1

0

Thanks to @dbc for pointing me in the right direction.

Deserializing polymorphic json classes without type information using json.net

Accepted answer of this link is the right answer.


Some precisions I'd like to add :

  • Using this solution, I had to remove struct properties of my abstract class because convert could not read them (I think it's possible but I prefered to extract struct's attributes to my abtrast class instead)
  • You may have a stackoverflow exception while reading incoming Json. You can solve this by using serializer.populate replacing JObject toItem method. OR, and this is what I did, apply a JSON NoConverter to concretes childs class

The NoConverter class which you can copy/paste and use it as attribute like every other JsonConverter

public class NoConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return false;
    }

    public override bool CanRead { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}