UnityのJsonUtilityの細かい10の疑問をいまさら検証した

# はじめに
Unity5.3から対応したJsonUtilityにそろそろ移行しようと思い、いまさらながら調査しました。
この記事では「JSONってなに？」という人もサポートするための概要編と
自分の疑問を検証した解決編に分けました。

# 概要編

## JSONとは

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf

JSONとはJavaScript Object Notationの略。直訳すると「JavaScriptのオブジェクト表記法」
以下の型を組み合わせてデータを記述します。

|型|表記／制限|サンプル|
|:--|:--|:--|
|null|nullを表す。小文字で表記する。||
|true|論理値のtrueを表す。小文字で表記する。||
|false|論理値のfalseを表す。小文字で表記する。||
|*number*|整数、浮動小数点数。10進法のみで指数表現可能|123|
|*string*|""でくくる。|"あいう"|
|*array*|valueを[]でくくる。valueは型のいずれか|["a","b","c"]|
|*object*|key-valueを:で区切り{}でくくる。keyはstring型。valueはいずれかの型|{"name":"john"}|

サンプルは以下の通りです。

```
{"profile":{"name":"ああああ","age":25,"好物":["焼肉","寿司"],"married":false,"jobs":null}}
```

## UnityのJsonUtilityとは

Unity5.3から対応した、JSONデータを操作するためのユーティリティクラス。
以下3つのメソッドを持ちます。(リファレンスより抜粋)
TextAssetを渡す場合はメインスレッドから呼び出される必要があります。

https://docs.unity3d.com/jp/540/ScriptReference/JsonUtility.html

|メソッド|概要|
|:--|:--|:--|
|FromJson|JSONからオブジェクトを作成します。|
|FromJsonOverwrite|JSONを読み取り、オブジェクトのデータを上書きします。|
|ToJson|オブジェクトのパブリックフィールドのJSON表現を生成します。|


### JSON変換ができるオブジェクトの条件

以下リファレンスに記載してあるように、変換可能なオブジェクトは「Serializableなプレーンクラスか構造体」となっています。

https://docs.unity3d.com/jp/540/ScriptReference/JsonUtility.FromJson.html

 - Serializable属性でマークされているプレーンなクラスか構造体でなければなりません。
 - オブジェクトのフィールドはシリアライザーでサポートされるタイプでなければなりません。
  - privateフィールドやNonSerialized属性をマークされるフィールドと同様にサポートされていない型を持つフィールドは無視されます。
 - プレーンなクラスと構造体だけがサポートされます。 
  - (MonoBehaviourやScriptableObjectなどのような) UnityEngine.Object から派生するクラスではないです。



### シリアライザーでサポートされるタイプとは

上のフィールドの条件で「シリアライザーでサポートされるタイプ」という単語が出てきました。
この詳細についてはリファレンスの別ページに記載されています。

https://docs.unity3d.com/jp/540/Manual/script-Serialization.html

- publicか[SerializeField]属性を持つ
- staticではないこと
- constではないこと
- readonlyではないこと
- シリアライズができるフィールドタイプ であること (以下を参照)

シリアライズできるフィールドタイプ

- [Serializable]属性を持つカスタム非抽象クラス
- [Serializable]属性を持つカスタム構造体
- UnityEngine.Objectから派生したオブジェクトの参照
- プリミティブ型(int, float, double, bool, string, etc.)
- シリアライズできるフィールドタイプの配列
- シリアライズできるフィールドタイプのList&lt;T&gt;

## UnityでJSON変換可能なオブジェクトを扱うには

上のJSONのサンプルをオブジェクトに変換できるクラスは以下のようになります。


```
[System.Serializable]
public class Profile
{
    public string name;
    public int age;
    public string[] 好物;
    public bool married;
    public string[] jobs;

    //serializableでないフィールドは出力されない
    [System.NonSerialized]
    public string[] hobbies;

    public static Profile CreateFromJSON(string json)
    {
        Profile profile = null;
        try {
            profile = JsonUtility.FromJson<Profile>(json);
        } catch (Exception e) {
            //例外を処理する場合
        }
        return profile;
    }

    public string ToJSON()
    {
        return JsonUtility.ToJson(this);
    }
}
```

# 解決編
大抵のことはJSONの仕様と上の概要編で貼っているリファレンスをよく読むと解決しますが
個人的に感じた疑問を実際にソースコードを書いて検証したものが以下になります。

### 1. JSON変換時に失敗したらどうなるか

変換対象でないフィールドがある場合、例外は吐かず無視されました。
(NonSerializedでSerializableでないフィールド)

変換対象にしているが変換不可なフィールドがある場合、ArgumentExceptionを吐きました。
(SerializeFieldでSerializableでないフィールド)

JsonUtility.FromJsonにnullを渡すと例外を吐かずnullが返ります。
JsonUtility.FromJsonに""を渡すと例外を吐かずnullが返ります。
JsonUtility.FromJsonに"[]"や"aaa"などobjectでないJSONを渡すとArgumentExceptionを吐きます。
JsonUtility.FromJsonに"{a:"など不正なJSONを渡すとArgumentExceptionを吐きます。

JsonUtility.FromJsonに"{}"を渡すと正常処理となります。

### 2. 数値型はどこまで扱えるのか

JSON仕様の「整数、浮動小数点数」の通りint, doubleを確認しました。
floatは"ToJsonの際に"誤差が生じます。←floatは"ToJsonの際に"誤差が生じます！
また、JSONの仕様の通り指数が扱えました。


```
{"floatTest":1.235E-001,"floatTest2":0.01,"doubleTest":1.235E-001}
```

↓ JsonUtility.FromJsonを行いフィールドとJsonUtility.ToJsonをデバッグ出力

<img width="664" alt="スクリーンショット 2017-05-16 21.06.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/19692/db8f880d-ae17-e68e-e28a-6829ad8f288d.png">


### 3. 日本語のフィールドは扱えるのか

Unityで認識される形式であれば問題なくJSONに変換されました。

### 4. JSONのキーと変数名を変更することは可能か
C#ではCamelCaseを使用しているがJSONはsnake_caseを書く規約になっている場合など。
gsonのSerializedNameのようにAttributeで指定する方法はないですが
ISerializationCallbackReceiverでSerialize/Deserializeのコールバックを取得して代用できます。

```
[System.Serializable]
public class Profile : ISerializationCallbackReceiver
{

:

    [SerializeField]  //JSONの変換に含む
    [HideInInspector] //SerializableだがInspectorに表示したくない
    private string favorite_foods;

    [System.NonSerialized] //JSONの変換に含まない
    public string favoriteFoods;

    //Serializeの前(=ToJsonの前)に呼ばれる
    public void OnBeforeSerialize()
    {
        favorite_foods = favoriteFoods;
    }

    //Deserializeの後(=FromJsonの後)に呼ばれる
    public void OnAfterDeserialize()
    {
        favoriteFoods = favorite_foods;
    }
}
```

### 5. Propertyのフィールドは扱えるか

シリアライザーでサポートされるタイプに入っていないので無視されます。
上記4と同様にISerializationCallbackReceiverで処理します。

### 6. Dictionaryのフィールドは扱えるか

シリアライザーでサポートされるタイプに入っていないので無視されます。
(リファレンスの通り、Genericで対応しているのはList&lt;T&gt;のみ)
上記4と同様にISerializationCallbackReceiverで処理します。

### 7. ScriptableObjectはJSON変換可能か

こちら試した所問題なくJSONに変換されました。
リファレンス上は「プレーンなクラスと構造体だけがサポートされます」
とのことなので自己責任で。

### 8. Unity標準で提供されている構造体を変換可能か

Vector3などでJSONに変換できることを確認しました。
全ては検証していないので自己責任で。

### 9. Unity標準で提供されているコンポーネント(の参照)を変換可能か

> 変換対象にしているが変換不可なフィールドがある場合、ArgumentExceptionを吐きました。

上記1に書いたとおりですがSerializeFieldにした場合、
出力(ToJson)は可能ですが読み込み(FromJson)を行うとArgumentExceptionを吐きました。

Spriteの場合

```
"sprite":{"instanceID":12022}}
```

### 10. rootがobject以外のJSONを変換できるか

> JsonUtility.FromJsonに"[]"や"aaa"などobjectでないJSONを渡すとArgumentExceptionを吐きます。

こちらも上記1に書いたとおりですが
変換できるのはrootがobject型のみ。その他の場合はArgumentExceptionを吐きました。

```
//ArgumentException
int number = JsonUtility.FromJson<int>("123");
List<int> list = JsonUtility.FromJson<List<int>>("[123,456,789]");
```

# まとめ

当然といえばそうですがJsonUtilityを扱うにはJSONの知識が必須でした。
JsonUtilityはJSONとUnityのシリアライザーの仕様を把握すればガッテンな感じでした。