Deserializing polymorphic json classes without type information using json.net

后端 未结 5 1068
暖寄归人
暖寄归人 2020-11-21 06:10

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

I can\'t see how to

相关标签:
5条回答
  • 2020-11-21 06:31

    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

    0 讨论(0)
  • 2020-11-21 06:32

    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);
            }
        }
    
    0 讨论(0)
  • 2020-11-21 06:39

    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; }
        }
    
    0 讨论(0)
  • 2020-11-21 06:41

    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 讨论(0)
  • 2020-11-21 06:47

    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.

    0 讨论(0)
提交回复
热议问题