Привет! Данная статья – это прежде всего напоминание для меня лично. Периодически возникает следующая задача. Есть иерархическая структура данных, представленная в формате 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))].
Теперь все отлично десериализуется в нужные нам типы. Пример можно посмотреть тут.
Комментариев нет:
Отправить комментарий