はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】System.Text.Jsonの「JsonDocumentとJsonElement」を利用して固定したSchemaがないJsonに対してデシリアライズを行う方法

はじめに

今回はSystem.Text.Jsonを用いてSchemaが変わる可能性があるようなJsonをデシリアライズする方法について紹介したいと思います。

www.nuget.org

↓利用される場面

  • 逆シリアル化する型がない
  • 受信した JSON に固定スキーマがなく、含まれている内容を確認するために検査する必要がある。

System.Text.Json で JSON DOM を使用する方法 - .NET | Microsoft Learn

概要

Schemaから判断して逆シリアル化できない場合は以下の2つの手法をとり JSON ドキュメント オブジェクト モデル(DOM)を構築する必要があります。

  • JsonDocumentJsonElementを利用する
  • JsonNode及びその派生クラスを利用する

今回はJsonDocumentJsonElementを利用した例を紹介したいと思います。

JsonDocument を使用すると、Utf8JsonReader を使用して読み取り専用 DOM を構築することができます。 ペイロードを構成する JSON 要素には、JsonElement 型を使用してアクセスできます。 JsonElement 型では、配列とオブジェクト列挙子と共に、JSON テキストを一般的な .NET 型に変換する API が提供されます。 JsonDocument では RootElement プロパティが公開されます。

System.Text.Json で JSON DOM を使用する方法 - .NET | Microsoft Learn

やり方

JsonDocumentの利用

JSON文字列もしくはUTF-8のバイト配列からJsonDocumentを生成します。

// 文字列から
string json = JsonSerializer.Serialize(target);
using JsonDocument document = JsonDocument.Parse(json);
            
// UTF-8のバイト配列から
Memory<byte> json = JsonSerializer.SerializeToUtf8Bytes(target);
using JsonDocument document = JsonDocument.Parse(json);

Parse内部で扱うのはUTF-8なので、可能であればstringは利用しない方が効率がいいはずです。

またJsonDocumentIDisposableを実装しているので、usingを忘れないでおきましょう。(もしくは直でDisposeを叩くか)

JsonElementの利用

Parseができたらデータを取り出します。例えば以下のようなJsonが入力として与えられた場合を考えます。
↓入力されるJson

{
  "Age": 40,
  "Name": "John"
}

これのAgeNameを取り出してみます。

using JsonDocument doc = JsonDocument.Parse(data);
int age = doc.RootElement
    .GetProperty("Age")
    .GetInt32();
string? name = doc.RootElement
    .GetProperty("Name")
    .GetString();

Console.WriteLine(age);     // 40
Console.WriteLine(name);    // John

JsonElement.GetPropertyによってAgeもしくはNameに対応するJsonElementを取得し、GetInt32()GetStringで値を取り出します。

見つからない時はエラーになる

ただ注意点として引数のpropertyNameに対応するプロパティが見つからない場合はエラーが出力されてしまいます。

Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.

エラーにしないためにはJsonElement.TryGetPropertyJsonElement.TryGetInt32を利用します。

using JsonDocument doc = JsonDocument.Parse(data);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("Age", out JsonElement ageJsonElement))
{
    if (ageJsonElement.TryGetInt32(out int age))
    {
        // 40
        Console.WriteLine(age);
    }
}
配列が列挙する

配列が含まれている場合はJsonElement.EnumerateArrayを利用します。
↓入力されるJson

{
  "Class Name": "Science",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ]
}
double sum = 0d;
            
using JsonDocument doc = JsonDocument.Parse(x);
JsonElement root = doc.RootElement;
JsonElement studentsElement = root.GetProperty("Students");
foreach (JsonElement student in studentsElement.EnumerateArray())
{
    if(student.TryGetProperty("Grade", out JsonElement gradeElement))
    {
        sum += gradeElement.GetDouble();
    }
}
           
// 67.92
Console.WriteLine($"Average : {sum / studentsElement.GetArrayLength()}");
プロパティを列挙する

プロパティを列挙するためにはJsonElement.EnumerateObjectを利用します。
↓入力されるJson

{
  "Class Name": "Science",
  "Students": [
    {
      "Name": "John",
      "Grade": 94.3
    },
    {
      "Name": "James",
      "Grade": 81.0
    },
    {
      "Name": "Julia",
      "Grade": 91.9
    },
    {
      "Name": "Jessica",
      "Grade": 72.4
    },
    {
      "Name": "Johnathan"
    }
  ]
}
using JsonDocument doc = JsonDocument.Parse(x);
JsonElement root = doc.RootElement;
            
foreach (JsonProperty jsonProperty in root.EnumerateObject())
{
  // ClassName, Students
  Console.WriteLine(jsonProperty.Name);
}