Привет! Данная статья – это прежде всего напоминание для меня лично. Периодически возникает следующая задача. Есть иерархическая структура данных, представленная в формате json, которую необходимо в объекты C#. При этом узлы дерева имеют разное значение и должны десериализоваться в разные классы. Как это сделать описано ниже.
Допустим, у нас есть файл c json следующей структуры:
Допустим, у нас есть файл c json следующей структуры:
{ "Roots": [ { "name": "My Documents", "type": "Folder", "nestedObjects": [ { "name": "Work", "type": "Folder", "nestedObjects": [ { "name": "Monthly Report", "type": "File", "fileType": "xlsx", "size" : 1024 } ] }, { "name": "YerlyReport", "type": "File", "fileType": "docx", "size" : 2048 } ] } ] }
Этот пример описывает древовидную структуру файлов и папок. Тип определяется полем type. Соответственно, для папок его значение “Folder”, для файлов – “File”. Папки описываются именем и массивом вложенных объектов. Файлы - именем, типом файла и размером в байтах.
Так как в папке могут быть как другие папки, так и файлы, то логично, что мы введем для них общий интерфейс, который дальше будут реализовывать файлы и папки. Для описания иерархии файлов и папок будем использовать паттерн Composite.
class FileContent { public List<ITreeNode> Roots { get; set; } } interface ITreeNode { string Name { get; set; } } class Folder : ITreeNode { public string Name { get; set; } public List<ITreeNode> NestedObjects { get; set; } } class File : ITreeNode { public string Name { get; set; } public int Size { get; set; } public string FileType { get; set; } }Для работы с json будем использовать популярную библиотеку Newtonsoft Json.NET. Код десериализации будет таким:
var tree = System.IO.File.ReadAllText("tree.json");
var reader = new JsonTextReader(new StringReader(tree));
FileContent content = JsonSerializer.CreateDefault().Deserialize<FileContent>(reader);
Вроде бы все логично и понятно, но если запустить этот код, то мы получим следующую ошибку: Could not create an instance of type JsonTreeDeserializer.ITreeNode. Type is an interface or abstract class and cannot be instantiated. Path 'Roots[0].name'. Причина проста: мы описали узел дерева с помощью интерфейса, а Json.NET не имеет дополнительной информации для того, чтобы определить экземпляр какого именно класса нужно создавать для каждого узла дерева.Это проблему можно решать несколькими способами:
- Включить в json-файл дискриминатор типа с полным именем типа .Net. Данный способ крайне неудобен, так как файл может поставлять из вне, и его поставщики могут ничего не знать о наших типах. Также при этом подходе мы становимся ограничены в изменении наименований и пространств имен нашил классов.
- Написать специализированный конвертер типов, который по описанию узла будет создавать экземпляр нужного класса и десериализовать в него данные. Данный подход удобен и мне нравится больше.
Для реализации конвертера необходимо создать наследника класса JsonConverter.
internal class TreeNodeConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var array = JArray.Load(reader); var target = new List<ITreeNode>(); if (array.HasValues) { foreach (var child in array.Children()) { var node = CreateNode(child); serializer.Populate(child.CreateReader(), node); target.Add(node); } } return target; } private ITreeNode CreateNode(JToken obj) { var type = (string)obj["type"]; switch (type) { case "Folder": return new Folder(); case "File": return new File(); default: throw new NotSupportedException(); } } public override bool CanConvert(Type objectType) { return false; } }Данный наследник реализует три метода:
- CanConvert – определяет, для каких типов должен использоваться конвертер. В данном случае он всегда возвращает false, так как мы будем применять конвертер к конкретным полям json с помощью атрибутов;
- WriteJson – сереализовать объект в json;
- ReadJson – десериализовать объект из json.
На этой части остановимся подробней. У меня получилось научить десериализовать коллекции наших узлов. Для этого мы получаем JArray из параметра reader. Создаем коллекцию, которую будем возвращать. Мы проходимся по всем элементам-токенам массива json. Из каждого токена извлекаем атрибут type и проверяем, какому типу он соответствует. Исходя из этого создаем экземпляр конкретного типа и с помощью метода Populate десериализуем в него данные из токена.
Чтобы конверт заработал, помечаем коллекцию FileContent.Roots и Folder.NestedObjects атрибутом [JsonConverter(typeof(TreeNodeConverter))].
Теперь все отлично десериализуется в нужные нам типы. Пример можно посмотреть тут.
Комментариев нет:
Отправить комментарий