воскресенье, 13 марта 2016 г.

Десериализация деревьев из Json

Привет! Данная статья – это прежде всего напоминание для меня лично. Периодически возникает следующая задача. Есть иерархическая структура данных, представленная в формате json, которую необходимо в объекты C#. При этом узлы дерева имеют разное значение и должны десериализоваться в разные классы. Как это сделать описано ниже.

Допустим, у нас есть файл 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 не имеет дополнительной информации для того, чтобы определить экземпляр какого именно класса нужно создавать для каждого узла дерева.
Это проблему можно решать несколькими способами:
  1. Включить в json-файл дискриминатор типа с полным именем типа .Net. Данный способ крайне неудобен, так как файл может поставлять из вне, и его поставщики могут ничего не знать о наших типах. Также при этом подходе мы становимся ограничены в изменении наименований и пространств имен нашил классов.
  2. Написать специализированный конвертер типов, который по описанию узла будет создавать экземпляр нужного класса и десериализовать в него данные. Данный подход удобен и мне нравится больше.
Для реализации конвертера необходимо создать наследника класса 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))].

Теперь все отлично десериализуется в нужные нам типы. Пример можно посмотреть тут.

Комментариев нет:

Отправить комментарий