Unityから複雑なJSONファイルを読み込む方法

## 前書き
友達に頼まれ、Unityでの作業を手伝う機会があったのですが、C#ではJson読み込みでひと工夫必要らしく、結構苦労して調べたので共有という感じの記事になります。

## 結論
[Newtonsoft](https://www.newtonsoft.com/json)というライブラリを使用します。

というのも、JsonUtilityなどでも読み込めることは読み込めますが、事前にjsonの構造に対応付けたクラスを作成する必要があったり、また例えば辞書の配列など複雑なデータ型は読み込めないようです。(NULLになる)

他の言語のように、json文字列から一発で辞書型に対応付けてほしいなぁと思っていたところ、Newtonsoftというライブラリを使用すると一発でできるそうだったので、以下このライブラリの説明になります。

### 使用する名前空間
```
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
```
ソースコード上部に上記を追加します。<br>
※ Unity2020からはデフォルトでインストールされているそうです

### 文字列からJObjectへの変換
```C#
string jsonText = "{ ... }"; // 何かしらのJSON文字列
JObject obj = JObject.Parse(jsonText); // これでJSONがまとめてパースされる
```

### JObjectの読み込み
これで`obj`変数にJSONのパース結果の`JObject`というオブジェクトが格納されました。  
また`JObject`の中には`JToken`というオブジェクトなどが入れ子になって入っているイメージになるそうです。

#### 参考ページ 
- [JObject公式リファレンス](https://www.newtonsoft.com/json/help/html/Overload_Newtonsoft_Json_Linq_JObject_GetValue.htm)
- [JToken公式リファレンス](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Linq_JToken.htm)
- [Newtonsoft Json.NETを雰囲気で使う](https://tech.packetroom.net/cs-newtonsoft-json-net-memo)

#### 真下のオブジェクトを取得
```C#
obj["key_name"]; // JTokenオブジェクトが帰ってくる
```

#### 真下の値を取得
```C#
obj["key_name"].Value<int>(); // intの所には要素の型名を入れる
```

#### 入れ子のオブジェクトを取得
```C#
obj["key1"]["key2"]; // JTokenオブジェクトが帰ってくる
```

#### 配列を取得
foreach文で回し、用意していたListにAddしていく感じになります。

```C#
string jsonText = "{ 'key1': [1, 2, 3] }";

var obj = JObject.Parse(jsonText);
var arr = new List<int>(); // 取得結果がこのリストに格納される

foreach (var jtoken in obj["key1"].Children())
{
    arr.Add(token.Value<int>());
}

Debug.Log(arr);
```

#### ネストした辞書を取得
`SelectToken()`というメソッドを使うと、ネストした辞書の要素にアクセス可能になります。  
取得結果は`JToken`などのオブジェクトになるので、`Values<型名>()`で変換します。

```C#
string jsonText = @"
{ 
    'key1': {
        'key2': {
            'key3': 10
        }
    }
}
";
var obj = JObject.Parse(jsonText);

var num = obj.SelectToken("key1.key2.key3").Value<int>();

// 10が出力される
Debug.Log(num);
```

#### 辞書の配列を取得
調べたところ`SelectToken("key1[0].value1")`などの書き方が可能とのこと。  
`SelectToken`の第一引数にはJSONPathというものを指定できるため。

- [JSONPath (Google検索)](https://www.google.com/search?q=jsonpath&oq=jsonpath&aqs=chrome..69i57j69i60l3.1336j0j1&sourceid=chrome&ie=UTF-8)
- [JSONPathの仕様について (英語)](https://docs.hevodata.com/sources/engg-analytics/streaming/rest-api/writing-jsonpath-expressions/)

`SelectToken`にJSONPathを指定してValue<型名>()を使い、要素を取得。
```C#
string jsonText = @"
{ 
    'key1': [
        {
            'key2': 'value2',
            'key3': 'value3'
        },
        {
            'key4': 'value4',
            'key5': 'value5'
        }
    ]
}
";
var obj = JObject.Parse(jsonText);

// key1[1]のようにSelectTokenでは添え字も指定可能
// JSONPathというXMLPathのような規格に準拠しているとのこと
var value5 = obj.SelectToken("key1[1].key5").Value<string>();

// "value5"が出力される
Debug.Log(value5);
```

## (参考) ソースコード例
``` C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;


public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // JSON文字列をファイルから読み込み
        string json = Resources.Load<TextAsset>("test").ToString();

        // JSON文字列をJTokenオブジェクトにパース
        JObject obj = JObject.Parse(json);

        // 辞書の配列を回す
        foreach (var dic in obj["part_candidates"])
        {
            Debug.Log(dic);
            
            // 辞書の中身を読み込む
            // {"1": [0.0, 0.1, 0.2], "2": [0.0, 0.1, 0.2]} のようなデータ
            foreach (var key in dic.Children())
            {
                // double配列の中身を保持するリスト
                List<double> arr = new List<double>();

                foreach (var value in key.Children())
                {
                    // 配列の中の小数を配列に追加する
                    foreach (var n in value.Children())
                    {
                        arr.Add(n.Value<double>());
                    }
                }
                double x = arr[0];
                double y = arr[1];
                double z = arr[2];

                Debug.Log(x + ", " + y + ", " + z);
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

```

以上