vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】【Unity】JSONデータを受け取りたかった話

はじめに

早いものでクリスマスももう終わり、 Advent Calender も無事?完了です。

ということで今日からは通常通り私が気になったあれやこれやを書いていくことにします。

なお Advent Calender の記事との違いは、「これは( Advent Calender 名)の n 日目の記事です」から始まらないことです。

それはそれとして。

今回は ASP.NET Core で生成した JSON データを Unity で受け取ろうとしたらうっかりハマったので、悔し紛れに書き残しておくことにします。

環境

  • .NET Core ver.2.2.101
  • Unity ver.2018.2.19f1
  • Utf8Json ver.1.3.7.1

Utf8Json について

これまで Unity で JSON データを扱うときは JsonUtility を使っていたのですが、今回は Utf8Json を使うことにしました。

github.com

理由としては、ウリとしている処理速度、というところもありますが、以下のようなところが ASP.NET Core と共通して書ける、ということが大きかったです。

  • 変換対象をプロパティで指定
  • 変換対象に List が使える
  • JSON が配列( List )になっていても変換できる
  • JSON <--> クラス の変換対象を DataMember で指定

Unity Package でインストールできるところも便利ですね。

Unity 2018.2 以上?の場合は unsafe コードがデフォルトで禁止されているため、 Build Settings > Other Settings > Configuration > Allow 'unsafe' Code にチェックを入れる必要があります。

ASP.NET Core のクラス

JSON に変換する ASP.NET Core のクラスはこのような感じにしました。

User.cs

using System;
using System.Runtime.Serialization;

namespace AspNetCoreTestSample.Models
{
    [DataContract]
    public class User
    {
        [DataMember]public int Id { get; set; }
        [DataMember]public string Name { get; set; }
        [DataMember]public DateTime LastUpdateDate { get; set; }
    }
}

Item.cs

using System.Collections.Generic;
using System.Runtime.Serialization;

namespace AspNetCoreTestSample.Models
{
    [DataContract]
    public class Item
    {
        [DataMember]public int Id { get; set; }
        [DataMember]public List< User> Users { get; set; }
    }
}

HomeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using AspNetCoreTestSample.Models;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCoreTestSample.Controllers
{
    public class HomeController: Controller
    {
        [Route("/items/all")]
        [Produces("application/json")]
        public List< Item> GetAllItems()
        {
            return new List< Item>
            {
                new Item
                {
                    Id = 0,
                    Users = new List< User>
                    {
                        new User
                        {
                            Id = 0,
                            Name = "User1",
                            LastUpdateDate = DateTime.Now,
                        },
                        new User
                        {
                            Id = 1,
                            Name = "User2",
                            LastUpdateDate = DateTime.Now,
                        },
                    }
                },
                new Item
                {
                    Id = 1,
                    Users = new List< User>
                    {
                        new User
                        {
                            Id = 2,
                            Name = "User3",
                            LastUpdateDate = DateTime.Now,
                        },
                        new User
                        {
                            Id = 3,
                            Name = "User4",
                            LastUpdateDate = DateTime.Now,
                        },
                    }
                }
            };
        }
    }
}

クラスが入れ子になっていたのもややこしさの原因だったのですよね……。

Firefox で生成したデータを見るとこのようになっています。 f:id:mslGt:20181226004359j:plain

この時点で察した方は鋭いです。

Unity のクラス

User.cs 、 Item.cs は ASP.NET Core と同じ内容です。

User.cs

using System;
using System.Runtime.Serialization;

namespace Models
{
    [DataContract]
    public class User
    {
        [DataMember]public int Id { get; set; }
        [DataMember]public string Name { get; set; }
        [DataMember]public DateTime LastUpdateDate { get; set; }
    }
}

Item.cs

using System.Collections.Generic;
using System.Runtime.Serialization;

namespace Models
{
    [DataContract]
    public class Item
    {
        [DataMember]public int Id { get; set; }
        [DataMember]public List< User> Users { get; set; }
    }
}

以前試した UnityWebRequest を使ってアクセスしてみます。

JsonLoader.cs

using System.Collections;
using System.Collections.Generic;
using Models;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Utf8Json;

public class JsonLoader : MonoBehaviour {

    public Button SendButton;

    public void Start() {
        SendButton.onClick.AddListener(() => {
            StartCoroutine(Send());
        });
    }
    private IEnumerator Send() {
        UnityWebRequest request = UnityWebRequest.Get(@"http://localhost:5000/items/all");
        yield return request.SendWebRequest();

        if(request.isNetworkError ||request.isHttpError) {
            Debug.Log(request.error);
        }
        else {
            List< Item> results = JsonSerializer.Deserialize< List< Item>>(request.downloadHandler.text);

            if (results == null)
            {
                Debug.Log("Results are null");
            }
            else if (results.Count <= 0)
            {
                Debug.Log("No items");
            }
            else
            {
                Debug.Log("There are some items");
            }
        }
    }
}
  • https の URL にアクセスしようとすると証明書の問題でエラーになるため http にアクセスしています。

これを実行すると、「There are some items」と出力されます。

なんだ簡単じゃないか。。。

問題

実はこの「results」、中身は全部デフォルト値が入っています。

そのため、「Debug.Log("There are some items");」の後下記のように書いてみると、 List< User> は空であることがわかります。

JsonLoader.cs

~省略~
else
{
    Debug.Log("There are some items");

    // 「Users are null」と出力される.
    if (results[0].Users == null)
    {
        Debug.Log("Users are null");
    }
    else if (results[0].Users.Count <= 0)
    {
        Debug.Log("No users");
    }
    else
    {
        Debug.Log("Success!! " + results[0].Users[0].Name);
    }
}
~省略~

「results」の数が合っていたりするため、なぜこのような結果になるのかがなかなかわかりませんでした。

解決

この原因は、最初に ASP.NET Core で生成された JSON の名前が、 LowerCamelCase になっていたことでした。

[{"id":0,"users":[{"id":0,"name":"User1","lastUpdateDate":"2018-12-25T23:51:47.6470112+09:00"}~省略~

これに対してクラスでの名前は UpperCamelCase であったため、この違いからうまく変換できていなかったようです。

ということで考えられる解決策をいくつか挙げてみます。

1. Unity 側のクラスの名前を LowerCamelCase にする

User.cs

using System;
using System.Runtime.Serialization;

namespace Models
{
    [DataContract]
    public class User
    {
        [DataMember]public int id { get; set; }
        [DataMember]public string name { get; set; }
        [DataMember]public DateTime lastUpdateDate { get; set; }
    }
}

確かに変換できるようにはなるのですが、命名規則に反することになるのであまりやりたくないですね。

2. Unity 側のクラスで DataMember の名前を指定する

User.cs

using System;
using System.Runtime.Serialization;

namespace Models
{
    [DataContract]
    public class User
    {
        [DataMember(Name = "id")]public int Id { get; set; }
        [DataMember(Name = "name")]public string Name { get; set; }
        [DataMember(Name = "lastUpdateDate")]public DateTime LastUpdateDate { get; set; }
    }
}

数が増えてくると大変ですが、何らかの理由で下記の 3. の方法が採れない場合に重宝しそうです。

3. ASP.NET Core 側で JSON の名前が UpperCamelCase で生成されるようにする

Startup.cs の ConfigureServices() で、 AddMvc() の戻り値である IMvcBuilder を使って JSON のオプションを指定できます。

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Serialization;

namespace AspNetCoreTestSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .AddJsonOptions(option =>
                {
                    option.SerializerSettings.ContractResolver = new DefaultContractResolver();
                });
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvc();
        }
    }
}

変更範囲が狭いため、可能であればこの方法が一番良さそうです。

おわりに

結果を見れば、大文字小文字で違ってるんだから問題が出て当然、という気はするのですが、それっぽいデータに騙されない、というのはなかなか難しいですね。

参照