Десеріалізація поліморфних класів json без інформації про тип за допомогою json.net


83

Цей виклик api Imgur повертає список, що містить як зображення Image Gallery, так і Gallery Gallery, представлені в JSON.

Я не можу зрозуміти, як десеріалізувати їх автоматично за допомогою Json.NET, оскільки немає властивості $ type, яка повідомляє десериалізатору, який клас повинен бути представлений. Існує властивість "IsAlbum", за допомогою якої можна розрізняти ці два.

Здається, це питання показує один із методів, але, схоже, це трохи хак.

Як розпочати десериалізацію цих класів? (за допомогою 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

1
Так, у мене є рядок json і я хочу десеріалізувати класи C #. Json.NET, схоже, використовує властивість, що називається $ type, щоб провести різницю між різними типами, що містяться в масиві. Ці дані не мають цієї властивості, а просто використовують властивість 'IsAlbum'.
Peter Kneale

Відповіді:


116

Ви можете зробити це досить легко, створивши спеціальний 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 / ...
Іван Krivyakov

5
Для тих, хто випробовує цей підхід і знаходить його, виникає нескінченний цикл (і, зрештою, переповнення стека), ви можете захотіти використовувати Populateпідхід замість ToObject. Див. Відповіді на stackoverflow.com/questions/25404202/… та stackoverflow.com/questions/29124126/… . У мене є приклад двох підходів у Gist тут: gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95 .
Кріс Олдвуд,

Я загубився у різних відповідях близько 8 годин, вони стосувались CustomCreationConverter. Нарешті ця відповідь спрацювала, і я відчуваю себе просвітленим. Мій внутрішній об'єкт містить його тип як рядок, і я використовую його для перетворення, як це. Елемент JObject = JObject.Load (читач); Тип типу = Тип.GetType (елемент ["Тип"]. Значення <string> ()); повернути елемент.ToObject (тип);
Furkan Ekinci

1
Це добре працює для мене за умови, що ви не ставите атрибут converter у базовий клас. Перетворювач повинен бути введений у серіалізатор (за допомогою налаштувань тощо) і перевірити лише базовий тип у CanConvert. Я обговорюю використання Populate (), але мені дуже не подобається жоден метод.
xtravar

1
Я полагодив конвертер для використання JsonSerializer.Populate()замість того, JObject.ToObject()як пропонували Іван та Кріс. Це дозволить уникнути проблем з рекурсивними циклами і дозволить перетворювачеві використовувати успішно з атрибутами.
Брайан Роджерс

39

Просто з атрибутами JsonSubTypes, які працюють з 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; }
    }

8
Це має бути головною відповіддю. Більшу частину дня я витрачав на вирішення цієї проблеми, вивчаючи власні класи JsonConverter від десятків авторів. Ваш nuget-пакет замінив усі ці зусилля трьома рядками коду. Молодці сер. Молодці.
Кеннет Кохран,

У світі Java бібліотека Джексона забезпечує подібну підтримку через @JsonSubTypesатрибут. Дивіться stackoverflow.com/a/45447923/863980 для іншого прикладу використання (також приклад Cage / Animal від @KonstantinPelepelin в коментарях).
вулкан-ворон

Це справді найпростіша відповідь, але, на жаль, це пов’язано з продуктивністю. Я з’ясував, що десериалізація відбувається в 2-3 рази швидше за допомогою рукописного конвертера (як показано у відповіді @BrianRogers)
Свен Вранкс

Привіт @SvenVranckx, сміливо відкривай випуск на github.com/manuc66/JsonSubTypes
manuc66

Приємна ідея, але це, на жаль, у мене не спрацювало. Я спробував різні запропоновані перестановки на сайті github, але врешті-решт мені потрібно було скористатися рукописним підходом.
satnhak

3

Відповідь просунутий до Брайана Роджерса . І про "використовувати Serializer.Populate () замість item.ToObject ()". Якщо похідні типи мають конструктори або деякі з них мають власний перетворювач, ви повинні використовувати загальний спосіб десеріалізації 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; змінити кешований об'єкт. Це лише змінює посилання на кешований об’єкт, і це безпечно для потоку.
Игорь Орлов

Це насправді дуже гарна відповідь, і вона робить все дуже простим. Якщо ви використовуєте Serializer.Populate (), вам потрібно створити об'єкт самостійно, і вся логіка створення об'єктів (наприклад, атрибути JsonConstructor) буде проігнорована.
Ерік А. Брандстадмоен

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]атрибут?
Alex McMillan,

1
Я припускаю, що ви можете просто відредагувати загальні налаштування JsonFormatter, щоб включити цей конвертер. Див stackoverflow.com/questions/41629523 / ...
xtravar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.