とあるREST APIのエンドポイントが /values/xxx
みたいなエンドポイントに対して次のような形式のJSONを返します(実際にはこの3倍くらい属性があり、listの中の配列も多数あります)。XXXで指定した名前の値のリストを取得するというイメージで、この名前のパターンは無数にあります。
{ "Value": { "Key1": "value1", "Data": [ { "Timestamp": "2020-10-29T13:02:00.000Z", "XXX": 123.45 }, { "Timestamp": "2020-10-29T13:03:00.000Z", "XXX": 456.78 } ] } }
つまり、XXXの部分が可変であるため、単純にC#のクラスにマッピングさせることができないのです。かといって、属性1つのためにJSON全体の構造をdynamicなりJsonDocument(JObject)として扱うのもつらいものがあります。
そこで使える機能が、クラスのメンバーに存在しないJSONのキーをC#側でDictionary型のオブジェクトとして保持することができます。この機能は、System.Text.JsonでもJson.NetでもJsonExtensionData
という同じ名前の属性で使えるのですが、名前空間と指定できる対象が異なります。
Json.NET では名前空間がNewtonsoft.Json
であり、プロパティもしくはフィールドに指定できます。
つぎのようなクラスを用意しておくと、JSONからC#に変換するときはTimestamp以外のJSONのキーとその値は_additionalData
に格納され、C#からJSONに変換するときはこの辞書のキーと値がJSONのキーと値として追加sれます。
public class Data { public DateTime Timestamp{ get; set; } [JsonExtensionData] private Dictionary<string, JToken> _additionalData; }
System.Text.Jsonでは名前空間がSystem.Text.Json.Serialization
であり、プロパティにしか指定できません。
public class Data { public DateTime Timestamp{ get; set; } [JsonExtensionData] public IDictionary<string, object> AdditionalData; }
このクラスに最初の例のJSONを変換すると、DictionaryにはキーがXXXで値が数値のエントリーが一つ入った状態になります。さて、まずはこれで目的を達したのですが、便利さのためにこのXXXというキーの名前とその数値をKeyName
とValue
というプロパティに格納することを考えましょう。
Json.NETではドキュメントに書いてある通り、OnDeserialized
属性を使って、C#からJSONへの変換が完了したときに実行できるコードをそのクラスの中に定義することができます。
public class Data { public DateTime Timestamp{ get; set; } public string KeyName { get; set; } public double Value { get; set; } [JsonExtensionData] private Dictionary<string, JToken> _additionalData; [OnDeserialized] private void OnDeserialized(StreamingContext context) { var first = segment.ExtensionData.FirstOrDefault(); if (first.Key != null) { KeyName = first.Key; Value = (double)first.Value; } } public DirectoryAccount() { _additionalData = new Dictionary<string, JToken>(); } }
これに対し、System.Text.Jsonではこのドキュメントにあるとおり、カスタムコンバーターを作成することで対応できます。
今回のケースだとこのようになるでしょう。
public class GetMetricSegmentCallbacksConverter : JsonConverter<GetMetricSegment> { public override GetMetricSegment Read( ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { GetMetricSegment result = JsonSerializer.Deserialize<GetMetricSegment>(ref reader); var first = result.ExtensionData.FirstOrDefault(); if (first.Key != null) { result.Key = first.Key; result.Value = (double)first.Value; } return result; } public override void Write( Utf8JsonWriter writer, GetMetricSegment result, JsonSerializerOptions options) { result.ExtensionData = new Dictionary<string, object>{{result.Key, result.Value}}; JsonSerializer.Serialize(writer, result); } }
いずれのケースも2つ以上オーバーフローしたキーが格納されているケースなどには対応できないので、適宜APIの仕様に合わせて対応しましょう。