雑記
 

VB/C#でJSONの読み込み (System.Text.Json編)

2020/4/4

この記事が対象とする製品・バージョン

VS2019 Visual Studio 2019 対象です。
VS2017 Visual Studio 2017 対象です。
VS2015 Visual Studio 2015 対象です。
VS2013 Visual Studio 2013 × 対象外です。
VS2012 Visual Studio 2012 × 対象外です。
VS2010 Visual Studio 2010 × 対象外です。
VS2008 Visual Studio 2008 × 対象外です。
VS2005 Visual Studio 2005 × 対象外です。
VS.NET 2003 Visual Studio 2003 × 対象外です。
VS.NET 2002 Visual Studio (2002) × 対象外です。

目次

1.概要

この記事では System.Text.Json を扱います。JSON.NETについては下記の記事で説明しています。

VB/C# でJSONの読み込み (JSON.NET編)

VB/C#でJSONを読み書きするには、JSON.NET(Newtonsoft JSON)またはSystem.Text.Jsonを使用するのが一般的です。

JSON.NETは多機能で使われる頻度が高く2020年3月現在ではデファクトスタンダードです。System.Text.Jsonは新しくMicrosoftが開発したライブラリで機能は少なめですがパフォーマンスが優れており、Webアプリケーションのスループットを向上させることができるようです。Webアプリケーションのように同時に多数の処理を動かすわけではない場合は、どちらを使っても問題なく高速です。

なお、 JSONの仕様については下記の記事で説明しています。

JSONの仕様

 

2.準備

System.Text.Jsonは、.NET Core 2.0以上 または .NET Framework 4.6.1 以上で使用できます。それより古い環境では使用できません。その場合はJSON.NETを使用してください。

System.Text.Jsonは、プロジェクトの種類によってははじめから使えるようになっていますが、そうでない場合NuGetからインストールする必要があります。

NuGetからインストールする場合 System.Text.Json で検索します。作成者は Microsoft です。

この記事で紹介する例はプログラムの冒頭にVBの場合、 Imports System.Text.Json が、C#の場合、 using System.Text.Json; が記述されていることが前提です。

 

3.基本的な考え方

System.Test.JsonではJSONの要素はすべて JsonElement クラスで表され、ValueKindプロパティでそれがオブジェクトなのか配列なのか数値なのか文字列なのかなどの区別を表します。

JSONでよく登場する 名前 : 値 のペア(たとえば、下記の例では "Width": 800 など)はJsonPropertyクラスで表現されます。JsonPropertyの名前(例 Width)はJSONの仕様上必ず文字列です。値(例 800)はJsonElementです。

下記のJSONを例に考えて見ます。

この例にはJsonElementがたくさん登場しますが、図ではいくつか抜粋して四角で囲ってみました。カッコ内はそのJsonElementのValueKindプロパティの値を示しています。

{ } で囲まれたオブジェクトは3つあり、これらを表すJsonElementはValueKindが JsonValueKind.Object になります。

それぞれのオブジェクト内に名前:値のぺアがあります。名前に着目して拾ってみると、"Image", "Width", "Height", "Title", "Thumbnail", "Url", "Height", "Width", "Animated", "IDs" の10個のペアがあることがわかります。これらの10個の値もそれぞれがJsonElementで表現されます。

名前の方は親オブジェクトを使って列挙させることができます。その例は後で紹介します。

値にはいろいろな型があるので理解を深めるために、名前とValueKindプロパティを一覧にして確認してみましょう。

名前 値を表すJsonElementのValueKindプロパティ
Image JsonValueKind.Object
Width JsonValueKind.Number
Height JsonValueKind.Number
Title JsonValueKind.String
Thumbnail JsonValueKind.Object
Url JsonValueKind.String
Height JsonValueKind.Number
Width JsonValueKind.Number
Animated JsonValueKind.False
IDs JsonValueKind.Array

最後の、IDsの値は [ ] で囲まれており、配列です。ValueKindプロパティは JsonValueKind.Array になります。

true, falseは論理型という枠ではなく、それぞれが JsonValueKind.True と JsonValueKind.False になります。このほかにこのJSONの例には登場しませんが、JsonValueKind.Null もあります。

 

System.Text.JsonでJSONを読み込んで処理をするには大きく分けて3つのアプローチがあります。

このJSONを例に簡単にどのようなアプローチかを紹介します。

 

アプローチ1:JSONの定義がわかっている場合1 - VB/C#のクラスにマッピングする

JSONの定義があらかじめわかっている場合はVB/C#で同じ構造のクラスを作成しておくと、System.Text.Jsonの機能でそのクラスに値を割り当ててくれます。クラスを定義するのは少し面倒かもしれませんが、読み込んだ後の処理では完全にVB/C#の世界になるのでJSONを意識することがありません。つまり、上述で説明したようなJsonElementやValueKindなどのことを知らなくてもよいということです。(とはいえ、いろいろ作っているとどうしても簡単にマッピングしただけでは済まない事態も発生するので、やはり、System.Text.Jsonがどういう考え方なのか上述の説明は理解しておいたほうが良いです。)

まず、上述のJSONをVB/C#のクラスで表現して次のような構造を作成します。プロパティ名は完全にJSONと一致させるのが楽ですが、クラス名は自由です。

Visual Studioの編集メニューから[形式を選択して貼り付け] - [JSONをクラスとして貼り付ける]を選択すると、JSONをVB/C#のクラスに変換して貼り付けてくれるのでこの手のクラスを定義する際に便利です。ただし、思ったように変換してくれない部分を手で修正する必要がある場合もよくあります。

VB

Public Class ProductInfo
    Public Property Image As ProductImage
End Class

Public Class ProductImage
    Public Property Width As Integer
    Public Property Height As Integer
    Public Property Title As String
    Public Property Thumbnail As ProductImageThumbnail
    Public Property Animated As Boolean
    Public Property IDs As Integer()
End Class

Public Class ProductImageThumbnail
    Public Property Url As String
    Public Property Height As Integer
    Public Property Width As Integer
End Class

C#

public class ProductInfo {
    public ProductImage Image { get; set; }
}

public class ProductImage {
    public int Width { get; set; }
    public int Height { get; set; }
    public string Title { get; set; }
    public ProductImageThumbnail Thumbnail { get; set; }
    public bool Animated { get; set; }
    public int[] IDs { get; set; }
}

public class ProductImageThumbnail {
    public string Url { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
}

 

後は簡単で、次のようにして、このクラスとJSONをマッピングできます。

マッピングするにはJsonSerializerクラスのDesrializeメソッドを使用し、型パラメーターにマッピング先の型を指定します。

VB

Private Function LoadJsonText() As String
    Dim json As New System.Text.StringBuilder
    json.AppendLine("{")
    json.AppendLine(" ""Image"": ")
    json.AppendLine(" {")
    json.AppendLine(" ""Width"": 800,")
    json.AppendLine(" ""Height"": 600,")
    json.AppendLine(" ""Title"": ""View from 15th Floor"",")
    json.AppendLine(" ""Thumbnail"": ")
    json.AppendLine(" {")
    json.AppendLine(" ""Url"": ""http://www.example.com/image/481989943"",")
    json.AppendLine(" ""Height"": 125,")
    json.AppendLine(" ""Width"": 100")
    json.AppendLine(" },")
    json.AppendLine(" ""Animated"" : false,")
    json.AppendLine(" ""IDs"": [116, 943, 234, 38793]")
    json.AppendLine(" }")
    json.AppendLine("}")

    Return json.ToString

End Function

Private Sub TestJson()

    Dim jsonText As String = Me.LoadJsonText
    Dim root As ProductInfo = JsonSerializer.Deserialize(Of ProductInfo)(jsonText)

End Sub

C#

private string LoadJsonText()
{
    StringBuilder json = new StringBuilder();
    json.AppendLine("{");
    json.AppendLine(" \"Image\": ");
    json.AppendLine(" {");
    json.AppendLine(" \"Width\": 800,");
    json.AppendLine(" \"Height\": 600,");
    json.AppendLine(" \"Title\": \"View from 15th Floor\",");
    json.AppendLine(" \"Thumbnail\": ");
    json.AppendLine(" {");
    json.AppendLine(" \"Url\": \"http://www.example.com/image/481989943\",");
    json.AppendLine(" \"Height\": 125,");
    json.AppendLine(" \"Width\": 100");
    json.AppendLine(" },");
    json.AppendLine(" \"Animated\" : false,");
    json.AppendLine(" \"IDs\": [116, 943, 234, 38793]");
    json.AppendLine(" }");
    json.AppendLine("}");

    return json.ToString();

}

private void TestJson()
{
    string jsonText = this.LoadJsonText();
    ProductInfo root = JsonSerializer.Deserialize<ProductInfo>(jsonText);
}

 

 

この例では、マッピングした後、何もしていませんが、root変数の内容を見ると、ちゃんと値が入っているのわかります。

ここまでできれば後はプログラムで自由に利用できますね。

 

アプローチ2:JSONの定義がわかっている場合2 - 位置を指定してアクセスする

JSONの定義がわかっているならば、クラスを定義しなくても値を読み取ることが可能です。

たとえば、次のようにしてUrlプロパティの値を読み込むことができます。

例にしているJSONの構造では、 UrlはImageが表すオブジェクト内で、Thumbailが表すオブジェクト内の Urlが表すオブジェクトの値である文字列であることがわかっているので、このプログラムではそれを取り出しています。

オブジェクトのプロパティの値はこの例のようにJsonElementクラスのGetPropertyメソッドを使用します。GetPropertyメソッドの戻り値はまたJsonElementクラスです。ここから値を取り出すにはGetString, GetBooleanなどのメソッドを使用します。

このプログラム中に登場する LoadJsonTextメソッドは上述のプログラム例に登場するものと同じなので割愛しています。

VB

Private Sub TestJson()
    Dim jsonText As String = Me.LoadJsonText
    Using document As JsonDocument = JsonDocument.Parse(jsonText)
        Dim root As JsonElement = document.RootElement
        Dim url As String = root.GetProperty("Image").GetProperty("Thumbnail").GetProperty("Url").GetString
    End Using
End Sub

C#

private void TestJson()
{
    string jsonText = this.LoadJsonText();

    using (JsonDocument document = JsonDocument.Parse(jsonText))
    {
        JsonElement root = document.RootElement;
        string url = root.GetProperty("Image").GetProperty("Thumbnail").GetProperty("Url").GetString();
    }
}

ところで、JSON.NETの場合、C#ではdynamicを使用して、自前のプロパティであるかのように(たとえば、root.Image.Thumbnail.Urlように)Jsonの要素にアクセスすすることができますが、System.Text.Jsonにはそのような機能はなく、この例のようなアクセス方法になります。

また、JsonPath(JSONパス)を使用した要素へのアクセスもできません。

JSONパスによるクエリ機能を追加する提案がされていますが、どうなるか2020年3月28日現在は未決です。

https://github.com/dotnet/runtime/issues/31068

 

アプローチ3:JSONの定義がわかっていない場合

JSONの定義があらかじめわかっていない場合、For Each(foreach)の列挙や、条件分岐、再帰などを使って構造を解析したり、値を検索、取得していくことになります。

このときに登場するのが前に説明したJsonElementクラスやValueKindプロパティです。

 

このプログラムでは読み込んでJSONの要素がオブジェクトなのか配列なのかその他なのかで分岐、オブジェクトの場合はプロパティの列挙、配列の場合は値の列挙、値の場合は値を表示するという処理を行っています。

上述のJSONでのみテストした簡易的な実装であり、対応できないケースもあるのではないかと思いますが、構造がわからないJSONに対するアプローチを示すのには十分だと思います。

VB

Private Sub TestJson()

    Dim jsonText As String = Me.LoadJsonText
    Using document As JsonDocument = JsonDocument.Parse(jsonText)
        Dim root As JsonElement = document.RootElement
        Parse(root)
    End Using

End Sub

Private Sub Parse(source As JsonElement)

    If source.ValueKind = JsonValueKind.Object Then
        Debug.WriteLine("(オブジェクト)")
        ParseObject(source)
    ElseIf source.ValueKind = JsonValueKind.Array Then
        Debug.WriteLine("(配列)")
        ParseArray(source)
    Else
        Debug.WriteLine(source)
    End If

End Sub

Private Sub ParseObject(source As JsonElement)

    For Each prop As JsonProperty In source.EnumerateObject
        Debug.Write($"{prop.Name} : ")
        Parse(prop.Value)
    Next

End Sub

Private Sub ParseArray(source As JsonElement)

    For Each value As JsonElement In source.EnumerateArray
        Parse(value)
    Next

End Sub

C#

private void TestJson()
{
    string jsonText = this.LoadJsonText();
    using (JsonDocument document = JsonDocument.Parse(jsonText))
    {
        JsonElement root = document.RootElement;
        Parse(root);
    }
}

private void Parse(JsonElement source)
{
    if (source.ValueKind == JsonValueKind.Object)
    {
        System.Diagnostics.Debug.WriteLine("(オブジェクト)");
        ParseObject(source);
    }
    else if (source.ValueKind == JsonValueKind.Array)
    {
        System.Diagnostics.Debug.WriteLine("(配列)");
        ParseArray(source);
    }
    else
    {
        System.Diagnostics.Debug.WriteLine(source);
    }
}

private void ParseObject(JsonElement source)
{
    foreach (var prop in source.EnumerateObject())
    {
        System.Diagnostics.Debug.Write($"{prop.Name} : ");
        Parse(prop.Value);
    }
}

private void ParseArray(JsonElement source)
{
    foreach (var value in source.EnumerateArray())
    {
        Parse(value);
    }
}

 

このプログラムを実行すると出力ウィンドウには次のように出力されます。

(オブジェクト)
Image : (オブジェクト)
Width : 800
Height : 600
Title : View from 15th Floor
Thumbnail : (オブジェクト)
Url : http://www.example.com/image/481989943
Height : 125
Width : 100
Animated : False
IDs : (配列)
116
943
234
38793

 

 

おまけ

JsonElementやJsonProperty、ValueKindなどの理解を深める練習用のサンプルを紹介しておきます。

Private Sub TestJson()

    Dim jsonText As String = Me.LoadJsonText

    Using document As JsonDocument = JsonDocument.Parse(jsonText)
        '一番外側の要素を取得
        Dim root As JsonElement = document.RootElement

        '一番側の要素はオブジェクトであることがわかっているのでプロパティ数を取得してみます。
        Debug.WriteLine($"一番外側のオブジェクトのプロパティ数:{root.EnumerateObject.Count}")

        '唯一のプロパティを取得。1つ目の要素があるとわかっているのでインデックス0で取得します。
        Dim prop1 As JsonProperty = root.EnumerateObject.First
        Debug.WriteLine($"プロパティ名:{prop1.Name}")
        Debug.WriteLine($"値の型:{prop1.Value.ValueKind.ToString}")

        'Imageプロパティの値を取得します。
        Dim middle As JsonElement = prop1.Value

        Debug.WriteLine($"真ん中のオブジェクトのプロパティ数:{middle.EnumerateObject.Count}")

        'プロパティを列挙します。オブジェクトであることがわかっているのでEnumerateObjectを使用します。
        For Each prop As JsonProperty In middle.EnumerateObject
            Debug.WriteLine($"プロパティ名:{prop.Name}")
            Debug.WriteLine($"プロパティの値:{prop.Value}")
            Debug.WriteLine($"値の型:{prop.Value.ValueKind.ToString}")
        Next

        '3番目のプロパティ(Title)を取得します。(2つ飛ばしたとことろにある1つを取得)
        Dim titleProp As JsonProperty = middle.EnumerateObject.Skip(2).Take(1).First
        Debug.WriteLine($"{titleProp.Name} : {titleProp.Value.GetString}")

        '最後のプロパティを取得します。
        Dim lastProp As JsonElement = middle.EnumerateObject.Last.Value
        '配列の値を列挙します。配列であることがわかっているのでEnumerateArrayを使用します。
        For Each value In lastProp.EnumerateArray
            Debug.WriteLine($"配列の値:{value}")
        Next

    End Using

End Sub

 

 

参考

.NET での JSON のシリアル化と逆シリアル化 (マーシャリングとアンマーシャリング)-概要

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-overview

 

Newtonsoft. Json から system.string に移行する方法

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to

 

Try the new System.Text.Json APIs

https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/