使用json.net反序列化没有类型信息的多态json类


77

Imgur api调用返回一个列表,其中包含以JSON表示的Gallery ImageGallery Album类。

鉴于没有$ type属性告诉反序列化器要表示哪个类,因此我看不到如何使用Json.NET自动反序列化。有一个称为“ IsAlbum”的属性,可用于区分两者。

这个问题似乎显示了一种方法,但看起来有点像个hack。

我该如何反序列化这些类?(使用C#,Json.NET)

样本数据:

图库图片

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

画廊专辑

{
    "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"
        }
    ]
}
}

您想将Json字符串放入类中吗?我对你的意思感到困惑there is no $type property
gunr2171 2013年

1
是的,我有json字符串,想反序列化为C#类。Json.NET似乎使用了名为$ type的属性来区分数组中保存的不同类型。此数据不具有该属性,而仅使用“ IsAlbum”属性。
Peter Kneale

Answers:


110

您可以通过创建JsonConverter用于处理对象实例化的自定义来相当容易地做到这一点。假设您的类定义如下:

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; }
}

您将这样创建转换器:

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();
    }
}

这是一个示例程序,显示了运行中的转换器:

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();
        }
    }
}

这是上面程序的输出:

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

小提琴:https : //dotnetfiddle.net/1kplME


20
如果多态对象是递归对象,即相册可能包含其他相册,则此方法不起作用。在转换器中,应该使用Serializer.Populate()而不是item.ToObject()。见stackoverflow.com/questions/29124126/…–
伊万·克里瓦雅科夫

5
对于尝试这种方法并发现它导致无限循环(并最终导致堆栈溢出)的任何人,您可能想要使用Populate而不是ToObject。请参阅stackoverflow.com/questions/25404202/…stackoverflow.com/questions/29124126/…的答案。我在此处的Gist中有两种方法的示例:gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95
克里斯·奥德伍德

大约8个小时后,我在各种答案中迷失了,他们是关于CustomCreationConverter的。终于,这个答案奏效了,我感到很高兴。我的内部对象包含它的类型为字符串,我用它来进行这样的转换。 JObject项目= JObject.Load(阅读器); 类型type = Type.GetType(item [“ Type”]。Value <string>()); 返回item.ToObject(type);
Furkan Ekinci

1
只要您不将converter属性放在基类上,这对我来说就很好。必须将转换器注入到序列化器中(通过设置等),并仅检查CanConvert中的基本类型。我正在使用Populate()进行辩论,但是我真的不喜欢这两种方法。
xtravar

我已经固定要使用的转换器,JsonSerializer.Populate()而不是JObject.ToObject()Ivan和Chris所建议的。这将避免递归循环的问题,并使转换器可以成功地与属性一起使用。
Brian Rogers

35

只需使用与Json.NET一起使用的JsonSubTypes属性

    [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; }
    }

7
这应该是最佳答案。我花了大部分时间研究这个问题的解决方案,研究了数十位作者的自定义JsonConverter类。您的nuget包用三行代码代替了所有工作。干得好先生。做得好。
肯尼斯·科克伦

在Java世界中,Jackson库通过@JsonSubTypesattribute提供类似的支持。有关另一个用例示例,请参见stackoverflow.com/a/45447923/863980(另请参见@KonstantinPelepelin中的笼子/动物示例)。
火神乌鸦

确实,这是最简单的答案,但不幸的是,这是以性能为代价的。我发现使用手写转换器的反序列化速度快2-3倍(如@BrianRogers的答案所示)
Sven Vranckx

@SvenVranckx您好,请随时在github.com/manuc66/JsonSubTypes
manuc66

2

Brian Rogers解答。关于“使用Serializer.Populate()代替item.ToObject()”。如果派生类型具有contstructor或其中一些具有自己的customconverter,则必须使用常规方法反序列化JSON。因此,您必须将工作实例化为NewtonJson的新对象。这样,您可以在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);
}

如果您有对象的递归,这项工作。


此代码在多线程上下文中不安全,因为默认合同解析器会缓存合同。在合同(如其转换器)上设置选项将导致其在并发甚至后续调用中的行为有所不同。
丹·戴维斯·布拉基特

serializer.ContractResolver.ResolveContract(DeterminedType)-返回已缓存的合同。所以contract.Converter = null; 更改缓存的对象。它仅更改对缓存对象的引用,并且是线程安全的。
ИгорьОрлов

1

后续实现应使您可以反序列化而无需更改设计类的方式,并且可以使用$ type以外的其他字段来决定将其反序列化为哪些内容。

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

我只发布此内容以消除一些混乱。如果您使用的是预定义格式并且需要反序列化,那么这是我发现最有效的方法,并演示了机制,以便其他人可以根据需要进行调整。

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);
        }
    }

在不使用[JsonConverter()]属性(被注释为“重要”)的情况下使用隐式转换时,如何使用它?例如:通过[FromBody]属性反序列化?
亚历克斯·麦克米伦

1
我假设您可以简单地编辑全局JsonFormatter的设置以包含此转换器。参见stackoverflow.com/questions/41629523/…–
xtravar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.