85

This Imgur api call returns a list containing both Gallery Image and Gallery Album classes represented in JSON.

I can't see how to deserialize these automatically using Json.NET given that there is no $type property telling the deserializer which class is meant to be represented. There is a property called "IsAlbum" that can be used to differentiate between the two.

This question appears to show one method but it looks like a bit of a hack.

How do I go about deserializing these classes? (using C#, Json.NET).

Sample Data:

Gallery Image

{
    "id": "OUHDm",
    "title": "My most recent drawing. Spent over 100 hours.",
        ...
    "is_album": false
}

Gallery Album

{
    "id": "lDRB2",
    "title": "Imgur Office",
    ...
    "is_album": true,
    "images_count": 3,
    "images": [
        {
            "id": "24nLu",
            ...
            "link": "http://i.imgur.com/24nLu.jpg"
        },
        {
            "id": "Ziz25",
            ...
            "link": "http://i.imgur.com/Ziz25.jpg"
        },
        {
            "id": "9tzW6",
            ...
            "link": "http://i.imgur.com/9tzW6.jpg"
        }
    ]
}
}
Nazim Kerimbekov
  • 3,965
  • 6
  • 23
  • 48
Peter Kneale
  • 1,502
  • 4
  • 17
  • 26
  • You want to take the Json string and put it into classes? And I'm confused by what you mean by `there is no $type property`. – gunr2171 Oct 10 '13 at 23:16
  • 1
    Yes, I have the json string and want to deserialize to C# classes. Json.NET appears to use a property called $type to draw a distinction between different types held in an array. This data doesnt have that property and just uses the 'IsAlbum' property. – Peter Kneale Oct 10 '13 at 23:34

5 Answers5

118

You can do this fairly easily by creating a custom JsonConverter to handle the object instantiation. Assuming you have your classes defined something like this:

public abstract class GalleryItem
{
    public string id { get; set; }
    public string title { get; set; }
    public string link { get; set; }
    public bool is_album { get; set; }
}

public class GalleryImage : GalleryItem
{
    // ...
}

public class GalleryAlbum : GalleryItem
{
    public int images_count { get; set; }
    public List<GalleryImage> images { get; set; }
}

You would create the converter like this:

public class GalleryItemConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(GalleryItem).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, 
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);

        // Using a nullable bool here in case "is_album" is not present on an item
        bool? isAlbum = (bool?)jo["is_album"];

        GalleryItem item;
        if (isAlbum.GetValueOrDefault())
        {
            item = new GalleryAlbum();
        }
        else
        {
            item = new GalleryImage();
        }

        serializer.Populate(jo.CreateReader(), item);

        return item;
    }

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

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

Here's an example program showing the converter in action:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
            {
                ""id"": ""OUHDm"",
                ""title"": ""My most recent drawing. Spent over 100 hours."",
                ""link"": ""http://i.imgur.com/OUHDm.jpg"",
                ""is_album"": false
            },
            {
                ""id"": ""lDRB2"",
                ""title"": ""Imgur Office"",
                ""link"": ""http://alanbox.imgur.com/a/lDRB2"",
                ""is_album"": true,
                ""images_count"": 3,
                ""images"": [
                    {
                        ""id"": ""24nLu"",
                        ""link"": ""http://i.imgur.com/24nLu.jpg""
                    },
                    {
                        ""id"": ""Ziz25"",
                        ""link"": ""http://i.imgur.com/Ziz25.jpg""
                    },
                    {
                        ""id"": ""9tzW6"",
                        ""link"": ""http://i.imgur.com/9tzW6.jpg""
                    }
                ]
            }
        ]";

        List<GalleryItem> items = 
            JsonConvert.DeserializeObject<List<GalleryItem>>(json, 
                new GalleryItemConverter());

        foreach (GalleryItem item in items)
        {
            Console.WriteLine("id: " + item.id);
            Console.WriteLine("title: " + item.title);
            Console.WriteLine("link: " + item.link);
            if (item.is_album)
            {
                GalleryAlbum album = (GalleryAlbum)item;
                Console.WriteLine("album images (" + album.images_count + "):");
                foreach (GalleryImage image in album.images)
                {
                    Console.WriteLine("    id: " + image.id);
                    Console.WriteLine("    link: " + image.link);
                }
            }
            Console.WriteLine();
        }
    }
}

And here is the output of the above program:

id: OUHDm
title: My most recent drawing. Spent over 100 hours.
link: http://i.imgur.com/OUHDm.jpg

id: lDRB2
title: Imgur Office
link: http://alanbox.imgur.com/a/lDRB2
album images (3):
    id: 24nLu
    link: http://i.imgur.com/24nLu.jpg
    id: Ziz25
    link: http://i.imgur.com/Ziz25.jpg
    id: 9tzW6
    link: http://i.imgur.com/9tzW6.jpg

Fiddle: https://dotnetfiddle.net/1kplME

Brian Rogers
  • 110,187
  • 27
  • 262
  • 261
  • 21
    This does not work if polymorphic objects are recursive, i.e. if an Album may contain other albums. In the converter one should use Serializer.Populate() instead of item.ToObject(). See http://stackoverflow.com/questions/29124126/polymorphic-json-deserialization-failing-using-json-net – Ivan Krivyakov May 11 '15 at 18:30
  • 5
    For anyone trying out this approach and finding it results in an infinite loop (and eventually a stack overflow) you might want to use the `Populate` approach instead of `ToObject`. See the answers to https://stackoverflow.com/questions/25404202/custom-inheritance-jsonconverter-fails-when-jsonconverterattribute-is-used and https://stackoverflow.com/questions/29124126/polymorphic-json-deserialization-failing-using-json-net . I have an example of the two approaches in a Gist here: https://gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95 . – Chris Oldwood Jun 04 '17 at 19:00
  • I was lost in various answers about 8 hours, they were about CustomCreationConverter. Finally this answer worked and I'm feeling enlightened. My inner object contains it's type as string and I use it to conversion like this. **JObject item = JObject.Load(reader); Type type = Type.GetType(item["Type"].Value()); return item.ToObject(type);** – Furkan Ekinci Sep 18 '17 at 06:55
  • 1
    This is working fine for me provided that you do not put the converter attribute on the base class. The converter must be injected into the serializer (via settings etc) and check for only the base type in CanConvert. I am debating using Populate() but I really don't like either method. – xtravar Sep 18 '17 at 20:28
  • 1
    I have fixed the converter to use `JsonSerializer.Populate()` instead of `JObject.ToObject()` as was suggested by Ivan and Chris. This will avoid problems with recursive loops and allow the converter to be used successfully with attributes. – Brian Rogers Sep 20 '19 at 23:13
  • This might be the greatest answer on Stack Overflow – A X Dec 12 '20 at 20:52
43

Simply with JsonSubTypes attributes that work with Json.NET

    [JsonConverter(typeof(JsonSubtypes), "is_album")]
    [JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
    [JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
    public abstract class GalleryItem
    {
        public string id { get; set; }
        public string title { get; set; }
        public string link { get; set; }
        public bool is_album { get; set; }
    }

    public class GalleryImage : GalleryItem
    {
        // ...
    }

    public class GalleryAlbum : GalleryItem
    {
        public int images_count { get; set; }
        public List<GalleryImage> images { get; set; }
    }
manuc66
  • 1,777
  • 20
  • 21
  • 8
    This should be the top answer. I've spent the better part of a day working on a solution to this problem examining custom JsonConverter classes from dozens of authors. Your nuget package replaced all that effort with three lines of code. Well done sir. Well done. – Kenneth Cochran Oct 11 '18 at 20:25
  • In Java world, Jackson library provides similar support via `@JsonSubTypes` attribute. See https://stackoverflow.com/a/45447923/863980 for another use-case example (also Cage/Animal example by @KonstantinPelepelin in comments). – vulcan raven Aug 19 '19 at 12:17
  • This is the simplest answer indeed, but unfortunately it comes at a performance cost. I found out that deserialization is 2-3 times faster with a hand-written converter (as shown in the answer of @BrianRogers) – Sven Vranckx Feb 16 '20 at 00:21
  • Hi @SvenVranckx, feel free to open an issue on https://github.com/manuc66/JsonSubTypes – manuc66 Feb 18 '20 at 09:26
  • Nice idea, but this didn't work for me unfortunately. I tried the various suggested permutations on the github site, but in the end I needed to go with the handwritten approach. – satnhak Jan 02 '21 at 14:56
3

Advanced to Brian Rogers answer. And about "use Serializer.Populate() instead of item.ToObject()". If derived types has contstructors or some of their has own customconverter you must use general way for deserialize JSON. So you must leave work for instantiate new object to NewtonJson. This way you can achieve it in you CustomJsonConverter:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    ..... YOU Code For Determine Real Type of Json Record .......

    // 1. Correct ContractResolver for you derived type
    var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
    if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
    {
        contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
    }

    // Deserialize in general way           
    var jTokenReader = new JTokenReader(jObject);
    var result = serializer.Deserialize(jTokenReader, DeterminedType);

    return (result);
}

This work if you have recursion of objects.

  • This code is not safe in mutithreaded contexts, because the default contract resolver caches contracts. Setting options on the contract (like its converter) will cause it to behave differently on concurrent, and even subsequent, calls. – Dan Davies Brackett Apr 15 '19 at 07:46
  • serializer.ContractResolver.ResolveContract(DeterminedType) - return already cached contract. So contract.Converter = null; change cached object. It only change reference on cached object, and it is thread safe. – Игорь Орлов Apr 16 '19 at 09:07
  • This is actually a very good answer, and it keeps everything very simple. If you use Serializer.Populate(), you need to create the object yourself, and all logic for creating the objects (e.g. JsonConstructor attributes) will be ignored. – Erik A. Brandstadmoen Nov 26 '20 at 10:20
1

Following implementation should let you de-serialize without changing the way you have designed your classes and by using a field other than $type to decide what to de-serialize it into.

public class GalleryImageConverter : JsonConverter
{   
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            if (!CanConvert(objectType))
                throw new InvalidDataException("Invalid type of object");
            JObject jo = JObject.Load(reader);
            // following is to avoid use of magic strings
            var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
            JToken jt;
            if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
            {
                return jo.ToObject<GalleryImage>();
            }
            var propValue = jt.Value<bool>();
            if(propValue) {
                resultType = typeof(GalleryAlbum);
            }
            else{
                resultType = typeof(GalleryImage);
            }
            var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
            var objectProperties=resultType.GetProperties();
            foreach (var objectProperty in objectProperties)
            {
                var propType = objectProperty.PropertyType;
                var propName = objectProperty.Name;
                var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
                if (token != null)
                {
                    objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
                }
            }
            return resultObject;
        }
        catch (Exception ex)
        {
            throw;
        }
    }

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

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

I'm only posting this to clear up some of the confusion. If you are working with a predefined format and need to deserialize it, this is what I found worked best and demonstrates the mechanics so that others can tweak it as needed.

public class BaseClassConverter : JsonConverter
    {
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var j = JObject.Load(reader);
            var retval = BaseClass.From(j, serializer);
            return retval;
        }

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

        public override bool CanConvert(Type objectType)
        {
            // important - do not cause subclasses to go through this converter
            return objectType == typeof(BaseClass);
        }
    }

    // important to not use attribute otherwise you'll infinite loop
    public abstract class BaseClass
    {
        internal static Type[] Types = new Type[] {
            typeof(Subclass1),
            typeof(Subclass2),
            typeof(Subclass3)
        };

        internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());

        // type property based off of class name
        [JsonProperty(PropertyName = "type", Required = Required.Always)]
        public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }

        // convenience method to deserialize a JObject
        public static new BaseClass From(JObject obj, JsonSerializer serializer)
        {
            // this is our object type property
            var str = (string)obj["type"];

            // we map using a dictionary, but you can do whatever you want
            var type = TypesByName[str];

            // important to pass serializer (and its settings) along
            return obj.ToObject(type, serializer) as BaseClass;
        }


        // convenience method for deserialization
        public static BaseClass Deserialize(JsonReader reader)
        {
            JsonSerializer ser = new JsonSerializer();
            // important to add converter here
            ser.Converters.Add(new BaseClassConverter());

            return ser.Deserialize<BaseClass>(reader);
        }
    }
xtravar
  • 1,251
  • 9
  • 24
  • How do you use this when using implicit conversion without using the [JsonConverter()] attribute (which is commented as "important")? Eg: deserializing via `[FromBody]` attribute? – Alex McMillan Nov 08 '17 at 20:46
  • 1
    I assume you can simply edit the global JsonFormatter's settings to include this converter. See https://stackoverflow.com/questions/41629523/using-a-custom-json-formatter-for-web-api-2 – xtravar Nov 08 '17 at 20:53