(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

(:3[kanのメモ帳]



UnityでJSONデータを操作(JSONとクラスを変換)するために使えるJsonUtilityとは【Unity】


このエントリーをはてなブックマークに追加


qqこの記事でのバージョン
Unity 2019.2.21f1


はじめに

Unityには簡単にJSONを扱える便利クラスJsonUtilityというものがあります。

JSON データを操作するためのユーティリティ関数


本ブログにも度々登場しているのですが、そう言えば単体で記事にした事はなかったので、

今回はJsonUtilityの概要や使い方などをまとめてみました!


なお、公式ドキュメントには「JSONデータを操作するための」とありますが、

どちらかというと「JSONとクラス(のインスタンス)を変換するための」クラスです。


JSONとは

そもそもJSONとはなんぞやという話ですが、

JSONとはJavaScript Object Notationの略で、XMLなどと同様のテキストベースのデータフォーマットです。

その名前の由来の通りJSONはJavaScriptのオブジェクト表記構文のサブセットとなっており、XMLと比べると簡潔に構造化されたデータを記述することができるため、記述が容易で人間が理解しやすいデータフォーマットと言えます。


上記の通り、データ表すフォーマットで、

以下のように左側にKey、右側に値を記述していく形式です。

{
    "_intValue": 58,
    "_floatValue": 12.5,
    "_boolValue": true,
    "_stringValue": "てきすと"
}


名前の通り、JavaScriptで使われていた(発祥?)ようですが、

そのわかり易さと使い勝手の良さからか、現在はUnityに限らず色々な所で使われています。


JsonUtilityで出来る事

さて本題のJsonUtilityの出来る事についてですが、なんとメソッド(=出来ること)は3つだけ。


1つ目がインスタンスをJSONに変換するToJsonです。

例えば以下のようなクラスがあった場合に、

[Serializable]
public class JsonClass {

  [SerializeField]
  private int _intValue = 58;
  public int IntValue => _intValue;
  
  [SerializeField]
  private float _floatValue = 12.5f;
  public float FloatValue => _floatValue;
  
  [SerializeField]
  private bool _boolValue = true;
  public bool BoolValue => _boolValue;

  [SerializeField]
  private string _stringValue = "てきすと";
  public string StringValue => _stringValue;

}


そのインスタンスをToJsonに渡すだけで、JSON(string)が取得できます。

//jsonにするクラスのインスタンス生成
JsonClass jsonClass = new JsonClass();
    
//JsonUtilityを使ってJSON化(第2引数をtrueにすると読みやすく整形される)
string json = JsonUtility.ToJson(jsonClass, prettyPrint:true);
    
//JSON確認
Debug.Log(json);
f:id:kan_kikuchi:20201118070036j:plain


2つ目がJSONからインスタンスを作成するFromJsonです。

ToJsonとは逆で、FromJsonにJSONを渡すだけでインスタンスが取得出来ます。

なお、全ての変数をJSONに記述する必要はありません。(例の場合は一つだけ)

//JSON作成
string json = "{\"_intValue\": 105}";

 //JsonUtilityを使ってJSONからJsonClass作成
JsonClass jsonClass = JsonUtility.FromJson<JsonClass>(json);
    
//インスタンスの内容確認
Debug.Log($"IntValue : {jsonClass.IntValue}, StringValue : {jsonClass.StringValue}");
f:id:kan_kikuchi:20201118070656j:plain


3つ目がJSONを読み取り、インスタンスのデータを上書きするFromJsonOverwriteです。

FromJsonOverwriteにJSONと値を上書きしたインスタンスを渡すだけですが、

ToJsonとは違い、元のJSONに全ての変数が記述されている必要あります。

//JSON作成
string json = "{\"_intValue\": 105}";

//JsonUtilityを使ってJSONからJsonClass作成
JsonClass jsonClass = JsonUtility.FromJson<JsonClass>(json);
    
//インスタンスの内容確認
Debug.Log($"IntValue : {jsonClass.IntValue}, StringValue : {jsonClass.StringValue}");
    
//新たなJSON作成
json = "{\"_intValue\": 58,\"_floatValue\": 12.5,\"_boolValue\": true,\"_stringValue\": \"テスト\"}";

//JsonUtilityを使って新たなJSONの内容を上書き
JsonUtility.FromJsonOverwrite(json, jsonClass);
    
//インスタンスの内容確認
Debug.Log($"IntValue : {jsonClass.IntValue}, StringValue : {jsonClass.StringValue}");
f:id:kan_kikuchi:20201118071313j:plain


なお公式によるとJsonUtilityはパフォーマンスが良いらしいです。

パフォーマンス
JsonUtilityは(機能は .NET JSON より少ないですが)、よく使用されている .NET JSON よりも著しく早いことが、ベンチマークテストで示されています。

GCメモリの使用が、以下のように最小に抑えられています。

ToJson()は、返されたストリングにのみ GC メモリをアロケーションします。
FromJson()は、返されたオブジェクトにのみ GC メモリをアロケーションします。必要な場合は、サブオブジェクトも同様です (例えば、配列を含むオブジェクトをデシリアライズする場合、GC メモリは配列にアロケーションされます)。
FromJsonOverwrite()は、書き込まれたフィールド (例えば、ストリングと配列) に必要な場合にのみ GC メモリをアロケーションします。JSON に上書きされたすべてのフィールドが値型の場合は、GC メモリはアロケーションされません。


またバックグラウンドで動かせるため、非同期処理を行う事も可能です。

JsonUtility API はバックグラウンドスレッドからの呼び出しが許可されています。そのため、一般的なマルチスレッドの制約と同じように、オブジェクトのシリアライズ/デシリアライズを行っている間に、他のスレッドから同じオブジェクトにアクセスしたり変更を加えたりしないように注意してください。




JSON化出来る条件

JsonUtilityはなんでもJSON化出来るわけではなく、条件があります。

その条件に当てはまらない物は以下のようにJSON化されませんし、

JSONからインスタンスを作る時にも値が反映されません。

f:id:kan_kikuchi:20201119063911j:plain


その条件とは「シリアライズ可能」というもので、

シリアライズ可能ならばクラスでも構造体でも変数でもJSONに出来ます。


なお、Unityでは「Inspectorに表示される物」というと分かりやすいかもしれません。

f:id:kan_kikuchi:20201119064109j:plain


具体的に言うと、クラスや構造体の場合はSerializableという属性が付いた物

[Serializable]//シリアライズ可能
public class JsonClass {

}


変数はpublicな物かSerializeFieldという属性が付いた物です。

ちなみにNonSerializedという属性を付ければpublicでもシリアライズ不可に出来ます。

//publicなのでシリアライズ可能
public int IntValue2 = 100;

//SerializeFieldが付いてるのシリアライズ可能
[SerializeField]
private int _intValue1 = 100;


//SerializeFieldが付いていないのでシリアライズ不可
private int IntValue3 = 100;

//NonSerializedが付いているのでシリアライズ不可
[NonSerialized]
public string IntValue4 = 100;


またシリアライズ可能なクラスや構造体が

さらにシリアライズ可能な物を持っている(入れ子になっている)場合も正常にシリアライズされます。

/*シリアライズ可能なJsonClassUnitが同じくシリアライズ可能なJsonClassを変数に持っている場合*/

[Serializable]
public class JsonClassUnit {

  [SerializeField]
  private JsonClass _jsonClass1 = new JsonClass(), _jsonClass2 = new JsonClass();
  
}

[Serializable]
public class JsonClass {

  [SerializeField]
  private int _intValue = 58;
  
}
f:id:kan_kikuchi:20201119065000j:plain


ただし、DictionaryやUnityのSpriteのようにそもそもシリアライズできない物は

publicにしたりSerializeFieldを付けてもシリアライズ出来ません。

(ちなみにListや配列はシリアライズ可能です。)

//publicだが、Dictionaryなのでシリアライズ不可
public Dictionary<string, int> _dict = new Dictionary<string, int>();

//Listや配列はシリアライズ可能
public List<int> _list = new List<int>();


なお、static、const、readonlyな物はシリアライズ出来ません


JSONとクラス(構造体)の形式の不一致

FromJsonの項目でちょっと触れましたが、

JSONの形式と変換先のクラス(構造体の形式が完全に一致していなくても問題ありません。


たとえば以下のような例で、JSONからクラスに変換すると、

{
    "SampleInt1": 111,
    "SampleInt2": 222
}
public int SampleInt2 = 2;
public int SampleInt3 = 3;


  1. SampleInt1はクラス側に存在しないので無視される。
  2. SampleInt2は両方に存在するのでJSONの値(222)が設定される。
  3. SampleInt3はJSONに存在しないのでクラス側の初期値(3)が設定される。



といった具合です。

なので、データを後から追加、削除してもエラーが出ないというわけです。


ただし、FromJsonOverwriteは完全に一致していないとエラーが出るのには注意が必要です。


変換(シリアライズ、デシリアライズ)時の処理

ISerializationCallbackReceiverを実装する事で、

シリアライズ前(JSON化前)に実行されるOnBeforeSerialize

デシリアライズ後(インスタンス化後)に実行されるOnAfterDeserializeを実装する事が出来ます。

シリアライズやデシリアライズ時にコールバックを受信するインターフェース


これを使えば、Dictionaryのようなシリアライズ出来ない物を

変換前後にシリアライズ出来る形式にしてJSON化するという事も可能です。

//保存したい値だが、Dictionaryなのでシリアライズ出来ない
public Dictionary<string, int> SampleDict = new Dictionary<string, int>(){
  {"Key1", 50},
  {"Key2", 150},
  {"Key3", 550}
};

//KeyとValueのペアを持ったクラス(Serializableが付いてるのでシリアライズ可能)
[Serializable]
private class Pair{
  public string Key;
  public int    Value;

  public Pair(KeyValuePair<string, int> pair){
    Key    = pair.Key;
    Value = pair.Value;
  }
}

//ペアクラスのリスト(Listなのでシリアライズ可能)
[SerializeField]
private List<Pair> _pairList = new List<Pair>();

//=================================================================================
//シリアライズ、デシリアライズ時の処理
//=================================================================================

/// <summary>
/// シリアライズ(インスタンス→JSON化)前に実行
/// </summary>
public void OnBeforeSerialize(){
  //Dictionaryの内容をシリアライズ可能な形に変換
  _pairList.Clear();
  foreach (var pair in SampleDict) {
    _pairList.Add (new Pair (pair));
  }
}

/// <summary>
/// デシリアライズ(JSON→インスタンス)後に実行
/// </summary>
public void OnAfterDeserialize(){
  if(_pairList.Count == 0){
    return;
  }

  //Dictionaryの内容を復元
  SampleDict.Clear();
  foreach (var pair in _pairList) {
    SampleDict [pair.Key] = pair.Value;
  }
}
{
    "_pairList": [
        {
            "Key": "Key1",
            "Value": 50
        },
        {
            "Key": "Key2",
            "Value": 150
        },
        {
            "Key": "Key3",
            "Value": 550
        }
    ]
}


ただこのやり方でも、保存したいDictionaryのkeyとvalueの組み合わせだけ

保存用クラスを作成しなければいけないため、結構面倒です。


JSONの書き出し(セーブ)と読み込み(ロード)

JsonUtilityの機能ではないのですが、

ついでにJSONの書き出しと読み込みの方法に付いても紹介しておきます。


まず書き出しですがSystem.IOのFile.WriteAllTextを使うと簡単に行えます。

using System.IO; //Fileクラスを使うのに必要
//JSONにするクラスのインスタンス生成
JsonClass jsonClass = new JsonClass();
    
//JsonUtilityを使ってJSON化(第2引数をtrueにすると読みやすく整形される)
string json = JsonUtility.ToJson(jsonClass, prettyPrint:true);

//Assets直下に書き出し(実機ではApplication.persistentDataPath等を使う)
File.WriteAllText ($"Assets/JsonClass.json", json);
f:id:kan_kikuchi:20201120072555j:plain


また、JSONはTextAssetとして読み込めるので、Resources.Load等を使えば簡単にロード可能です。

//Resources直下にあるJSONをTextAsset形式でロード
TextAsset jsonText = Resources.Load<TextAsset>("JsonClass");

 //JsonUtilityを使ってJSONからJsonClass作成
JsonClass jsonClass = JsonUtility.FromJson<JsonClass>(jsonText.text);


ちなみに、これを応用すればクラスをまるごとJSON化して保存するみたいな事も可能です





参考