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
コードはこちらのリポジトリにあります。
導入
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 を許可します。
内容
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);
内容
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; } } } }