78

given this JSON:

[
  {
    "$id": "1",
    "$type": "MyAssembly.ClassA, MyAssembly",
    "Email": "me@here.com",
  },
  {
    "$id": "2",
    "$type": "MyAssembly.ClassB, MyAssembly",
    "Email": "me@here.com",
  }
]

and these classes:

public abstract class BaseClass
{
    public string Email;
}
public class ClassA : BaseClass
{
}
public class ClassB : BaseClass
{
}

How can I deserialize the JSON into:

IEnumerable<BaseClass> deserialized;

I can't use JsonConvert.Deserialize<IEnumerable<BaseClass>>() because it complains that BaseClass is abstract.

pb2q
  • 58,613
  • 19
  • 146
  • 147
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231

5 Answers5

104

You need:

JsonSerializerSettings settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All
};

string strJson = JsonConvert.SerializeObject(instance, settings);

So the JSON looks like this:

{
  "$type": "System.Collections.Generic.List`1[[MyAssembly.BaseClass, MyAssembly]], mscorlib",
  "$values": [
    {
      "$id": "1",
      "$type": "MyAssembly.ClassA, MyAssembly",
      "Email": "me@here.com",
    },
    {
      "$id": "2",
      "$type": "MyAssembly.ClassB, MyAssembly",
      "Email": "me@here.com",
    }
  ]
}

Then you can deserialize it:

BaseClass obj = JsonConvert.DeserializeObject<BaseClass>(strJson, settings);

Documentation: TypeNameHandling setting

poke
  • 369,085
  • 72
  • 557
  • 602
Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • @AndrewBullock do you know what the parameters of `$type` are? I found a lot of examples with it but nowhere an explanation. Why do you need to specify `MyAssembly. ClassA` and then again `MyAssembly`? The linked `TypeNameHandling` page doesn't provide an explanation to this mystery either :( – t3chb0t Oct 22 '15 at 17:09
  • surely the last line here is incorrect, you have a `List` that you are trying to deserialize to a `BaseClass` should the last line not read `var obj = JsonConvert.DeserializeObject>(strJson, settings)` – MikeW Sep 02 '16 at 11:35
  • is it possible to make this work when `"$type": "MyAssembly.ClassB, MyAssembly",` is not contained in the json and use custom criteria to determine the derived object type to deserialise to using another criteria? – Chinwobble Jan 11 '17 at 11:39
  • @Chinwobble have a look at https://stackoverflow.com/questions/11099466/using-a-custom-type-discriminator-to-tell-json-net-which-type-of-a-class-hierarc – manuc66 Jun 20 '17 at 09:39
  • 4
    Be careful, this opens up your endpoint to security issues: https://www.alphabot.com/security/blog/2017/net/How-to-configure-Json.NET-to-create-a-vulnerable-web-API.html – gjvdkamp Aug 13 '18 at 10:59
  • 2
    Good Solution, But before applying read this https://www.alphabot.com/security/blog/2017/net/How-to-configure-Json.NET-to-create-a-vulnerable-web-API.html – Abhinesh Oct 08 '18 at 07:24
  • `$type` information should be sanitized for security. See [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954) for details. – dbc Feb 28 '19 at 23:39
38

Here is a way to do it without populating $type in the json.

A Json Converter:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        if (jo["FooBarBuzz"].Value<string>() == "A")
            return jo.ToObject<AFoo>(serializer);

        if (jo["FooBarBuzz"].Value<string>() == "B")
            return jo.ToObject<BFoo>(serializer);

        return null;
    }

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

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

using it:

var test = JsonConvert.DeserializeObject<List<BaseFoo>>(result, new JsonSerializerSettings() 
{ 
    Converters = { new FooConverter() }
});

taken from here

chris31389
  • 8,414
  • 7
  • 55
  • 66
  • 3
    This answer is excellent, but will error if 'FooBarBuzz' is missing (i.e. you need consistently formatted json). You can also check the field exists as shown here - https://stackoverflow.com/a/19158793/852806 – JsAndDotNet May 02 '18 at 13:07
7

use the following JsonSerializerSettings construct while deserializing :

new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.Objects
})
Steven
  • 166,672
  • 24
  • 332
  • 435
Sunil S
  • 391
  • 3
  • 5
  • 1
    `$type` information should be sanitized for security. See [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954) for details. – dbc Feb 28 '19 at 23:39
0

You could also wrap the enumerable in a class:

class Wrapper
{
    IEnumerable<BaseClass> classes;
}

then serialize and deserialize this.

Andrew Bullock
  • 36,616
  • 34
  • 155
  • 231
  • I have the same problem as you, and this doesnt work for me (as I already am doing this), it complains for the same reason when you deserialize it. – Zwik Mar 31 '14 at 15:53
-2

As I needed only one-side serializer for specific base class (to make API return derived classes properties), I came up with current solution

public class CustomConverter : JsonConverter<BaseClass>
{
    private readonly JsonSerializerOptions _serializerOptions;

    public CustomConverter()
    {
        _serializerOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            IgnoreNullValues = true,
        };
    }

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

    public override BaseClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, BaseClass value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(JsonSerializer.SerializeToUtf8Bytes(value, value.GetType(), _serializerOptions));
    }
}
Denis
  • 17
  • 1
  • 9