yotiky Tech Blog

とあるエンジニアの備忘録

Unity (HoloLens) - Json シリアライザの基本的な使い方

Unity (HoloLens) で Json をパースするライブラリのサンプルコード集です。

目次

概要

対象クラス

対象とするライブラリ(クラス)は次の通りです。

ライブラリ(クラス) 提供元 備考
JsonUtility Unity Unity純正で速いが制約が多い
Json.NET(Newtonsoft.Json) Newtonsoft C#におけるメジャーどころ、遅いが汎用的で拡張性がある
Utf8Json neuecc氏 JsonUtilityと同様に速く、更に汎用的

Newtonsoft はニュージーランドにある企業のようです。「Json.NET」がプロダクト名で、Newtonsoft.Json はライブラリ名(名前空間)といったところでしょうか。nuget では、Newtonsoft.Json の名前で公開されています。

Utf8Json は、残念ながら HoloLens2(IL2CPP x ARM64)で動かすことはできませんでした。Editor 上では動くので実装例だけですが掲載してあります。

これら以外に .NET Standard 2.0 以降で使える System.Text.Json もありますが、こちらは導入がサクッと行かず、加えてパフォーマンスも良くないという話なので早めに見切りをつけました。

開発環境

  • Unity : 2019.3.15f.1
    • Scripting Backend:IL2CPP
    • Platform:UWP ARM64
  • Visual Studio : 2019
  • Json.NET:12.0.3
  • Utf8Json:1.3.7.1
  • HoloLens 2

コードはこちらのリポジトリにあります。

github.com

導入

JsonUtility

Unity 純正のため、何もしなくても使えます。

Json.NET (Newtonsoft.Json)

公式サイトはこちらリポジトリこちらにあります。
Asset Store にも Json.NET がありますが、v8ベースの古いもの(現在 v12)なので使わないようにしましょう。

リポジトリリリースから zip ファイルをダウンロードします。
NuGet Gallery の右にある Download からパッケージをダウンロードして、拡張子を zip に変更することもできます。

zip ファイル内の [lib/netstandard2.0] の配下にある Newtonsoft.Json.dll を、Unityプロジェクトの [Assets\Plugins] にコピーします。([lib/net45] だと動きません。)

Assets フォルダに以下の内容の link.xml を追加します。

<linker>
  <assembly fullname="System.Core">
    <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
  </assembly>
</linker>

同じ解説がここにもあります。

Utf8Json

リポジトリこちら

リポジトリリリースから unitypackage と CodeGenerator をダウンロードします。

パッケージをUnityにインポートしたら、Project Setting > Player で unsafe code を許可します。

f:id:yotiky:20200622221913p:plain

検証しようとしている環境ではこの時点でエラーが出てしまうので暫定対応を取ります。

内容
IsConstructedGenericType() 拡張メソッドが見つからない。

対処
Reflection.cs の最後に拡張メソッドを追加しました。
#else
namespace System.Reflection
{
    internal static class ReflectionExtensions
    {
        public static bool IsConstructedGenericType(this TypeInfo type)
        {
            return type.IsConstructedGenericType;
        }
    }
}
#endif

サンプル

シリアライズ対象のクラスには適当な ToString メソッドでオーバーライドしています。記事で見やすいようにインデントも調整しています。

JsonUtility

JsonUtility の実装例です。 まずシリアライズ対象のクラスです。

クラスに Serializable 属性を付ける必要があります。メンバーはフィールドのみが対象です。プロパティは使えません。

[Serializable]
public class PersonSerializableClassField
{
    public int id;
    public AddressSerializableClassField[] addresses;
}

[Serializable]
public class AddressSerializableClassField
{
    public int zipcode;
    public string address;
}

シリアライズとデシリアライズの実装例です。

        var target = new PersonSerializableClassField
        {
            id = 100,
            addresses = new AddressSerializableClassField[]
            {
                new AddressSerializableClassField{ zipcode = 1002321, address = "hoge" },
                new AddressSerializableClassField{ zipcode = 1008492, address = "fuga" },
            },
        };

        var serialized = JsonUtility.ToJson(target);
        Debug.Log(serialized);

        var deserialized = JsonUtility.FromJson<PersonSerializableClassField>(serialized);
        Debug.Log(deserialized.ToString());

シリアライズ結果です。

{
    "id":100,
    "addresses":[
    {"zipcode":1002321,"address":"hoge"},
    {"zipcode":1008492,"address":"fuga"}
    ]
}

Json.NET (Newtonsoft.Json)

Json.NET のシリアライズ対象のクラスですが、 JsonUtility のように細かな制約はありません。 フィールドに限らずプロパティも可能で、クラス、フィールド、プロパティに属性をつける必要もありません。

JsonUtility で使ったクラスも勿論そのまま使えます。

[Serializable]
public class PersonSerializableClassField
{
    public int id;
    public AddressSerializableClassField[] addresses;
}

[Serializable]
public class AddressSerializableClassField
{
    public int zipcode;
    public string address;
}

プロパティを使った例です。

public class PersonPlaneClassProperty
{
    public int Id { get; set; }
    public AddressPlaneClassProperty[] Addresses { get; set; }
}

public class AddressPlaneClassProperty
{
    public int Zipcode { get; set; }
    public string Address { get; set; }
}

シリアライズとデシリアライズの基本的な実装例です。 JsonConvert クラスを使います。

JsonUtilityで使用したクラスです。

    var target = new PersonSerializableClassField
    {
        id = 100,
        addresses = new AddressSerializableClassField[]
        {
            new AddressSerializableClassField{ zipcode = 1002321, address = "hoge" },
            new AddressSerializableClassField{ zipcode = 1008492, address = "fuga" },
        },
    };

    var serialized = JsonConvert.SerializeObject(target);
    Debug.Log(serialized);

    var deserialized = JsonConvert.DeserializeObject<PersonSerializableClassField>(serialized);
    Debug.Log(deserialized.ToString());

次にプロパティの例です。呼び出すコードは全く同じです。

    var target = new PersonPlaneClassProperty
    {
        Id = 200,
        Addresses = new AddressPlaneClassProperty[]
        {
            new AddressPlaneClassProperty{ Zipcode = 2003921, Address = "hoge" },
            new AddressPlaneClassProperty{ Zipcode = 2002955, Address = "fuga" },
        },
    };

    var serialized = JsonConvert.SerializeObject(target);
    Debug.Log(serialized);

    var deserialized = JsonConvert.DeserializeObject<PersonPlaneClassProperty>(serialized);
    Debug.Log(deserialized.ToString());

出力結果です。

{
    "Id":200,
    "Addresses":[
        {"Zipcode":2003921,"Address":"hoge"},
        {"Zipcode":2002955,"Address":"fuga"}
    ]
}

JsonProperty 属性を使うと Jsonシリアライズするときに別名を与えられます。また、JsonIgnore 属性をつけるとシリアライズ対象外として処理されます。

public class AddressRenamedProperty
{
    [JsonProperty("Foo")]
    public int Zipcode { get; set; }

    [JsonProperty("Bar")]
    public string Address { get; set; }

    [JsonIgnore]
    public string TelephoneNumber { get; set; }
}

出力結果です。

{
    "Foo":3003924,
    "Bar":"foobar"
}

Json.NET では、JObjectクラスを使うとデシリアライズ結果にインデクサでアクセスすることも可能です。予め静的なクラスを用意できない場合には有用です。

Utf8Json

こちらも汎用的なクラスを流し込めます。

JsonUtility な Serializable のクラスです。

[Serializable]
public class PersonSerializableClassField
{
    public int id;
    public AddressSerializableClassField[] addresses;
}

[Serializable]
public class AddressSerializableClassField
{
    public int zipcode;
    public string address;
}

プロパティを使ったクラスです。

public class PersonPlaneClassProperty
{
    public int Id { get; set; }
    public AddressPlaneClassProperty[] Addresses { get; set; }
}

public class AddressPlaneClassProperty
{
    public int Zipcode { get; set; }
    public string Address { get; set; }
}

シリアライズとデシリアライズの実装例です。 JsonSerializer クラスを使います。

    var target = new PersonSerializableClassField
    {
        id = 100,
        addresses = new AddressSerializableClassField[]
        {
            new AddressSerializableClassField{ zipcode = 1002321, address = "hoge" },
            new AddressSerializableClassField{ zipcode = 1008492, address = "fuga" },
        },
    };

    var serialized = JsonSerializer.Serialize(target);
    Debug.Log(Convert.ToBase64String(serialized));

    var deserialized = JsonSerializer.Deserialize<PersonSerializableClassField>(serialized);
    Debug.Log(deserialized.ToString());

Utf8Json は文字列への変換を飛ばしてbyte[]に直接叩き込むことで高いパフォーマンスを実現しています。そのため serialized 変数を直接ログに出力しても byte[] しか表示されません。

System.Byte[]

Convert.ToBase64String(serialized); すると次のようになります。

eyJpZCI6MTAwLCJhZGRyZXNzZXMiOlt7InppcGNvZGUiOjEwMDIzMjEsImFkZHJlc3MiOiJob2dlIn0seyJ6aXBjb2RlIjoxMDA4NDkyLCJhZGRyZXNzIjoiZnVnYSJ9XX0=

Json である以上互換性が大事ということで、次の方法でbyte[] から文字列の Json に復元できます。

    var tmp = System.Text.Encoding.UTF8.GetString(serialized);
    Debug.Log(tmp);

出力結果です。

{
    "id":100,"
    addresses":[
        {"zipcode":1002321,"address":"hoge"},
        {"zipcode":1008492,"address":"fuga"}
    ]
}

バイナリではなく、Json 文字列に変換することもできます。

      var serialized = JsonSerializer.ToJsonString(target);
    Debug.Log(serialized);

    var deserialized = JsonSerializer.Deserialize<PersonSerializableClassField>(serialized);
    Debug.Log(deserialized.ToString());

serialized の出力結果です。

{
    "id":100,
    "addresses":[
        {"zipcode":1002321,"address":"hoge"},
        {"zipcode":1008492,"address":"fuga"}
    ]
}

次にプロパティの例です。呼び出すコードは全く同じです。

    var target = new PersonPlaneClassProperty
    {
        Id = 200,
        Addresses = new AddressPlaneClassProperty[]
        {
            new AddressPlaneClassProperty{ Zipcode = 2003921, Address = "hoge" },
            new AddressPlaneClassProperty{ Zipcode = 2002955, Address = "fuga" },
        },
    };

    var serialized = JsonSerializer.Serialize(target);
    Debug.Log(Convert.ToBase64String(serialized));
    var tmp = System.Text.Encoding.UTF8.GetString(serialized);
    Debug.Log(tmp);

    var deserialized = JsonSerializer.Deserialize<PersonPlaneClassProperty>(serialized);
    Debug.Log(deserialized.ToString());

シリアライズ結果に別名を与えるには、DataMember 属性を使います。IgnoreDataMember 属性でシリアライズ対象外になります。

public class AddressRenamedProperty
{
    [DataMember(Name = "Foo")]
    public int Zipcode { get; set; }

    [DataMember(Name = "Bar")]
    public string Address { get; set; }

    [IgnoreDataMember]
    public string TelephoneNumber { get; set; }
}

Utf8Json では dynamic にデシリアライズすることができます。dynamic を使うことでインデクサでのアクセスが可能になります。

デプロイ

Utf8Json

実機で動かすに至らなかったため覚書です。

Unity IL2CPPで動かすには事前にコードジェネレーターを実行する必要があります。
リポジトリからダウンロードした Utf8Json.UniversalCodeGenerator.zip を解凍し、次のコマンドを実行します。

Utf8Json.UniversalCodeGenerator.exe -d "YourTargetDirectories" -o "Utf8JsonGenerated.cs"

作成されたファイルをUnityプロジェクトに移動し、アプリケーションの初期化処理など最初の方に実行されるコードのあたりで Resolver を登録する処理を追加します。

    Utf8Json.Resolvers.CompositeResolver.RegisterAndSetAsDefault(
        Utf8Json.Resolvers.GeneratedResolver.Instance,
        Utf8Json.Resolvers.StandardResolver.Default);
検証しようとしている環境ではGeneratedしたファイルでエラーが出てしまうので暫定対応を取ります。

内容
Utf8Json.Resolvers.DynamicCompositeResolver は抽象クラスのため new できない。

対処
DynamicCompositeResolver.Create メソッドを呼ぶようにし、DynamicCompositeResolverに無理やりキャストして返す。
実機ではここまで処理が到達しなかったのでこれで動くようになるのかはかなり怪しい。
    var ____result = (global::Utf8Json.Resolvers.DynamicCompositeResolver)global::Utf8Json.Resolvers.DynamicCompositeResolver.Create(__formatters__, __resolvers__);

ここまでで立ちはだかるエラーは次の通りです。NotSupportedExceptionとあるように根本的に何かを変えてあげないと動かない予感がします。

NotSupportedException: System.AppDomain::DefineDynamicAssembly
  at System.AppDomain.DefineDynamicAssembly (System.Reflection.AssemblyName name, System.Reflection.Emit.AssemblyBuilderAccess access) [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Internal.Emit.DynamicAssembly..ctor (System.String moduleName) [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DynamicObjectResolverAllowPrivateFalseExcludeNullFalseNameMutateOriginal..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.DynamicObjectResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver+InnerResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.StandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 
Rethrow as TypeInitializationException: The type initializer for 'Utf8Json.Resolvers.Internal.DynamicObjectResolverAllowPrivateFalseExcludeNullFalseNameMutateOriginal' threw an exception.
  at Utf8Json.Resolvers.DynamicObjectResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver+InnerResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.StandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 
Rethrow as TypeInitializationException: The type initializer for 'Utf8Json.Resolvers.DynamicObjectResolver' threw an exception.
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver+InnerResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.StandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 
Rethrow as TypeInitializationException: The type initializer for 'Utf8Json.Resolvers.Internal.DefaultStandardResolver.InnerResolver' threw an exception.
  at Utf8Json.Resolvers.Internal.DefaultStandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at Utf8Json.Resolvers.StandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 
Rethrow as TypeInitializationException: The type initializer for 'Utf8Json.Resolvers.Internal.DefaultStandardResolver' threw an exception.
  at Utf8Json.Resolvers.StandardResolver..cctor () [0x00000] in <00000000000000000000000000000000>:0 
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 
Rethrow as TypeInitializationException: The type initializer for 'Utf8Json.Resolvers.StandardResolver' threw an exception.
  at JsonSamples.Start () [0x00000] in <00000000000000000000000000000000>:0 

OnPreBuild

Generator の出力結果がそのままではビルドが通らなかったため、毎回上書きされてもその直後にビルドエラーになることが必須であまり効果的ではありませんでしたが、次の方法でビルドに仕込むこともできます。

すべてをコードで実行することもできますが、次のような Utf8JsonGenerate.bat を用意しました。 バッチは Unity プロジェクトのルートに置いてます。Utf8Json.UniversalCodeGenerator はルートに Utf8json_gen フォルダを作ってそこに一式コピーしてあります。

utf8json_gen\Utf8Json.UniversalCodeGenerator.exe -d "Assets\Scripts" -o "Assets\Scripts\Utf8JsonGenerated.cs"

続いて、Unityのビルド前処理でバッチを実行するように実装します。 これで、Unityをビルドするたびに Utf8Json.UniversalCodeGenerator が実行されます。ただし、最初の一発目は Resolver を登録する必要があるため、ビルド前処理を仕込むよりも先に実行する必要があります。

public class BuildProcessor : IPreprocessBuildWithReport
{
    public int callbackOrder => 0;

    public void OnPreprocessBuild(BuildReport report)
    {
        var info = new ProcessStartInfo();
        info.FileName = "cmd.exe";
        info.Arguments = $"/c " + "Utf8JsonGenerate.bat";
        info.WorkingDirectory = Environment.CurrentDirectory;
        info.WindowStyle = ProcessWindowStyle.Hidden;
        info.UseShellExecute = false;
        info.RedirectStandardOutput = true;

        Process process = null;
        try
        {
            process = Process.Start(info);
            Debug.Log(process.StandardOutput.ReadToEnd());
            process.WaitForExit();
        }
        finally
        {
            if (process != null)
            {
                if (!process.HasExited)
                {
                    process.Kill();
                }
                process.Dispose();
                process = null;
            }
        }
    }
}