概要
Jsonでネストされている部分のメンバーをネストされていないメンバーと同じレベルで扱いたい。
通常はネストされている部分ごとにObjectを定義すべきだが、
- 最初はスクレイピングでデータを取ってきていたけど後からAPIを立ててデータをJsonでやり取りするようにした
- すでにアプリで使用しているローカルのデータベース構造をあんまりいじるとデータの整合性を保つのが困難
というクラス設計の甘さからきた苦し紛れの解決策です。
サンプルのJsonファイル
employee.json
{
"id":1
"name": "Suzukaze Aoba",
"company": {
"id":1
"name": "EagleJump",
"department": {
"id":1
"name": "Graphic"
}
},
"projects": [
{
"id":1
"name": "FAIRIES STORY"
},
{
"id":2
"name": "PECO"
}
]
}
DeserializeさせたいObject
Employee.cs
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string CompanyName { get; set; } //ここに"company"配下の"name"を割り当てたい
public string DepartmentName { get; set; } // ここに"company"配下の更に"department"配下の"name"を割り当てたい
public string ProjectName { get; set; } //ここに"projects"の最初の要素の"name"を割り当てたい
}
解決策
JsonConverterを継承したクラスを作り、Deserialize時に変換操作を行わせる
ObjectMemberPicker.cs
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class ObjectMemberPicker : JsonConverter
{
private string _memberPath = "";
public ObjectMemberPicker(string memberPath)
{
this._memberPath = memberPath;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
return jo.SelectToken(this._memberPath).ToObject(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
ArrayMemberPicker.cs
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
class ArrayMemberPicker:JsonConverter
{
private string _memberPath = "";
public ArrayMemberPicker(string memberPath)
{
this._memberPath = memberPath;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JArray jo = JArray.Load(reader);
return jo.SelectToken(this._memberPath).ToObject(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Converter適用後のDeserializeさせたいObject
Employee.cs
using Newtonsoft.Json;
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
// JsonPropertyでDeserializeしたいメンバーを持つJsonObject名を指定
// JsonConverterの宣言でObjectMemberPickerの型と指定したいPathをパラメーターで渡す
// (このパラメーターがObjectMemberPickerのコンストラクタへ引数として渡される)
[JsonProperty("company")]
[JsonConverter(typeof(ObjectMemberPicker), "name")]
public string CompanyName { get; set; }
// 一つのJsonPropertyは一つのPropertyにしか割り当てられないようなので、
// 今のところDepartmentNameにDeserializeするならComopanyNameは諦めるしかない
// Deserialize自体は下記のPath指定でできた。
//[JsonProperty("company")]
//[JsonConverter(typeof(ObjectMemberPicker), "department.name")]
//public string DepartmentName { get; set; }
[JsonProperty("projects")]
[JsonConverter(typeof(ArrayMemberPicker), "[0].name")]
public string ProjectName { get; set; }
}
まとめ
とりあえず当初の目的はクリアできた。
あとは
- 同レベルで扱いたいネストされたメンバーが複数になった時
をどうするか・・・・
参考にしたstackoverflowの記事では、クラスにConverterを指定する形式で
もうちょっと複雑なロジックを組んでいて、JsonProperty自体にPathを設定していたけど、
プロパティごとにConverterを切り替えたかったりするときにうまく動かなかった。
何か良い方法はないのだろうか・・・
テストに使ったコードを置いておきますのでアドバイスあればお願い致します。
DeserialiseTest.cs
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
[TestClass]
public class DeserializeTest
{
[TestMethod]
public void EmployeeDeserializeTest()
{
var employeeJson = "{\"id\":1,\"name\":\"SuzukazeAoba\",\"company\":{ \"id\":1,\"name\":\"EagleJump\",\"department\":{ \"id\":1,\"name\":\"Graphic\"} },\"projects\":[{\"id\":1,\"name\":\"FAIRIES STORY\"},{\"id\":2,\"name\":\"PECO\"}]}";
var employee = JsonConvert.DeserializeObject<Employee>(employeeJson);
Assert.AreEqual(1, employee.ID);
Assert.AreEqual("SuzukazeAoba", employee.Name);
Assert.AreEqual("EagleJump", employee.CompanyName);
//Assert.AreEqual("Graphic", employee.DepartmentName);
Assert.AreEqual("FAIRIES STORY", employee.ProjectName);
}
}