【Unity】JSONデータを送信するときの処理メモ

# 詰まったこと
Unityクライアントアプリから、Laravelで開発しているサーバサイドへjson文字列を送る処理で
WWWFormにJsonUtilityでJSON文字にしたデータを送ろうとしたとき、サーバで受け取る時には抜け落ちていた。
原因と対応策についての備忘録。

#事象発生時の実装

スキルの効果量を計算するため、パーティメンバの現在のステータス、敵のステータス、補正を掛けるときに使う数値等々をクラスで持っており、JsonUtilityで文字列を生成する。

``` C#

ownerJson = JsonUtility.ToJson(owner);
scoreJson = JJsonUtility.ToJson(new ScoreInfo());
partyJson = "[";
enemyJson = "[";

int pIndex = 0;
party.ForEach(p =>
{
    if (pIndex > 0) partyJson += ",";
    partyJson += JsonConvert.SerializeObject(p);
    pIndex++;
});
partyJson += "]";

int eIndex = 0;
enemyParty.ForEach(e =>
{
    if (eIndex > 0) enemyJson += ",";
    enemyJson += JsonConvert.SerializeObject(e);
    eIndex++;
});
enemyJson += "]";

skillJson = JsonConvert.SerializeObject(skill);
```

POST通信でJSON文字列を送る時の処理。WWWFormを作成し、UploadHandlerRawでbodyにセット
``` C#
 public async Task<List<string>> getBattleEffectVal(
        string ownerJson,
        string partyJson, 
        string enemyJson, 
        string scoreJson,
        string skillJson,
        int skillCount,
        string dataTable)
    {
        string url = apiPath + API_BATTLE_EFFECT_CALC;

        WWWForm form = new WWWForm();
        form.AddField("owner", ownerJson);
        form.AddField("party", partyJson);
        form.AddField("enemy", enemyJson);
        form.AddField("dataTable", dataTable);
        form.AddField("score", scoreJson);
        form.AddField("skill", skillJson);
        form.AddField("skillCount", skillCount);

        return await sendApi<string>(url, false, form);
    }

    /// <summary>
    /// APIリクエストを投げる
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="url">APIリクエストURL</param>
    /// <param name="isGet">GET通信か</param>
    /// <param name="form">Form情報</param>
    /// <param name="bodyBytes">Bodyに設定するByte情報</param>
    /// <returns></returns>
    private async Task<List<T>> sendApi<T>(string url, bool isGet, WWWForm form)
    {
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

        List<T> responses = new List<T>();
        UnityWebRequest request = isGet ? UnityWebRequest.Get(url) : UnityWebRequest.Post(url, form);

        // 識別番号を渡してアセットバンドル名を取得
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Accept", "application/json");
        request.SetRequestHeader("Authorization", "Bearer " + User.ApiToken);

        if(form != null)
        {
            request.uploadHandler = new UploadHandlerRaw(form.data);
        }

        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError)
        {
            Debug.Log($"通信エラー:{request.method}  {url} {request.responseCode}");
            Debug.Log(request.result);
            Debug.Log(request.error);
            Debug.Log(request.downloadHandler.text.ToString());
            Debug.Log(form.data);

            return null;
        }
//後続処理
```

## この状態で送信したときのbody情報
Larabel側に出ていたデバッグログ

``` log
~~ヘッダ情報等など~~
"x-unity-version":["2022.1.20f1"],
"content-length":["10932"]},
"body":[]} ※空！
```

生成したJSON文字をPostmanでBodyにセットした場合はしっかり出ている。
``` log
"content-length":["7055"]},
"body":{"owner":"{\"status\":{\"characterId\":0,\"level\":1,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":1,\"useType\":1,\"actions\":[{\"instanceID\":31862}]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":1,\"useType\":2,\"actions\":[]},\"partyIndex\":0,\"race\":1,\"isPlayer\":true,\"characterClass\":1}","party":"[{\"status\":{\"characterId\":0,\"level\":1,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":1,\"useType\":1,\"actions\":[{\"instanceID\":31862}]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":1,\"useType\":2,\"actions\":[]},\"partyIndex\":0,\"race\":1,\"isPlayer\":true,\"characterClass\":1},{\"status\":{\"characterId\":0,\"level\":2,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":1,\"race\":3,\"isPlayer\":false,\"characterClass\":52},{\"status\":{\"characterId\":0,\"level\":2,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":2,\"race\":2,\"isPlayer\":false,\"characterClass\":11},{\"status\":{\"characterId\":0,\"level\":2,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":3,\"race\":2,\"isPlayer\":false,\"characterClass\":50}]","enemy":"[{\"status\":{\"characterId\":0,\"level\":1,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":0,\"race\":2,\"isPlayer\":false,\"characterClass\":0},{\"status\":{\"characterId\":0,\"level\":2,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":1,\"race\":2,\"isPlayer\":false,\"characterClass\":0},{\"status\":{\"characterId\":0,\"level\":3,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":2,\"race\":2,\"isPlayer\":false,\"characterClass\":0},{\"status\":{\"characterId\":0,\"level\":5,\"discription\":\"\",\"health\":{\"maxHealth\":{\"value\":100,\"valueDouble\":0.0},\"health\":{\"value\":10,\"valueDouble\":0.0}},\"power\":{\"value\":0,\"valueDouble\":0.0,\"powerEffect\":0},\"mental\":{\"value\":0,\"valueDouble\":0.0,\"mentalEffect\":0},\"critical\":{\"value\":0,\"valueDouble\":0.0,\"criticalEffect\":0.0},\"rebornCount\":{\"maxRebornCount\":0,\"rebornCount\":0}},\"nomalSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"actionSkill\":{\"skillName\":\"\",\"discription\":\"\",\"count\":0,\"battleType\":5,\"useType\":1,\"actions\":[]},\"partyIndex\":3,\"race\":2,\"isPlayer\":false,\"characterClass\":0}]","score":"{\"judge\":[{\"judgeName\":\"BAD\",\"score\":0,\"lernScore\":[0,0,0,0]},{\"judgeName\":\"GOOD\",\"score\":0,\"lernScore\":[0,0,0,0]},{\"judgeName\":\"GREAT\",\"score\":0,\"lernScore\":[0,0,0,0]},{\"judgeName\":\"PARF\",\"score\":0,\"lernScore\":[0,0,0,0]},{\"judgeName\":\"MISS\",\"score\":0,\"lernScore\":[0,0,0,0]}],\"combo\":{\"combo\":0,\"maxCombo\":0,\"lernCombo\":[0,0,0,0],\"lernMaxCombo\":[0,0,0,0]},\"score\":0.0}","skill":"{\"actionName\":\"単体攻撃\",\"discription\":\"敵一体に3倍攻撃\",\"skillType\":1,\"side\":false,\"args\":[3.0],\"results\":[],\"dataTable\":\"{owner.pow}*{skill.args.0}\"}","skillCount":"10","dataTable":"{owner.pow}*{skill.args.0}"}} 

[2023-07-23 22:05:47] local.INFO: Response: {"status_code":200,"headers":{"cache-control":["no-cache, private"],
"date":["Sun, 23 Jul 2023 13:05:47 GMT"],
"content-type":["text/html; charset=UTF-8"],"x-ratelimit-limit":[60],"x-ratelimit-remaining":[59]},"body":"0+0*3"} 
```

## 原因
WWWFormに文字列をセットするサンプル（ChatGPTに出してもらった）で直接文字列を宣言し、
それをFormデータにセットするとbodyから抜け落ちることなくサーバ側に渡されていた。

違いとしては、「""」で囲んだ状態（エスケープされている状態）になっているかという点。

```
//JsonUtilityで出力した文字列
dataTable:{owner.pow}*{skill.args.0}　
※実際ははDebug.Log($"dataTable:{dataTable}")で出している
→サーバログ：body":[]}
　
//直宣言した文字列
string test = "{\"dataTable\":\"{owner.pow}*{skill.args.0}\"}";
{"dataTable":"{owner.pow}*{skill.args.0}"}
→サーバログ："body":{"dataTable":"{owner.pow}*{skill.args.0}"}
```

JsonUtilityは値を「""」で囲めていない。
だからといって以下のようにしてもbodyにはセットされず…
``` C#
WWWForm form = new WWWForm();
form.AddField("owner", ownerJson);
form.AddField("party", partyJson);
form.AddField("enemy", enemyJson);
form.AddField("dataTable", "\"{owner.pow}*{skill.args.0}\"");
form.AddField("score", scoreJson);
form.AddField("skill", skillJson);
form.AddField("skillCount", skillCount);
```

# 解決策
WWWFormは使わず、文字列配列を作成して全部合体させたうえでバイトデータをbodyにセットする。
``` C#
        List<string> formDatas = new List<string>()
        {
            $"\"owner\":{ownerJson}",
            $"\"party\":{partyJson}",
            $"\"enemy\":{enemyJson}",
            $"\"score\":{scoreJson}",
            $"\"skill\":{skillJson}", //dataTableはskillJsonに含まれる
            $"\"skillCount\":{skillCount}"
        };

        string jsonArray = "{" + string.Join(",", formDatas) + "}";
        byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonArray);
```

``` C#
private async Task<List<T>> sendApi<T>(string url, bool isGet, WWWForm form, byte[] body=null)
    {
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

        List<T> responses = new List<T>();
        UnityWebRequest request = isGet ? UnityWebRequest.Get(url) : UnityWebRequest.Post(url, form);

        // 識別番号を渡してアセットバンドル名を取得
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Accept", "application/json");
        request.SetRequestHeader("Authorization", "Bearer " + User.ApiToken);
        request.downloadHandler = new DownloadHandlerBuffer();

        if (form != null)
        {
            request.uploadHandler = new UploadHandlerRaw(form.data);
        }
        if(body != null)
        {
            //jsonBytesを引数bodyで渡してセット
            request.uploadHandler = new UploadHandlerRaw(body);
        }
```
無事に計算結果を取得
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2796558/f659c666-c7db-9661-9ce3-80a4ceb68202.png)

WWWFormでうまく渡す方法も探します…。

# 補足
JsonUtilityの代わりにJsonConvert.SerializeObjectでエスケープ付の文字列生成できるようです。
エラーが出て確認できなかったので、機会あれば追記します。

#　追記（2023/8/13）
修正後の例ではjsonをそのまま渡していますが、key:valueのvalueが単純な文字列の場合
「"」をエスケープして囲む必要があります。忘れないようにメモ。
```
List<string> formDatas = new List<string>()
        {
            $"\"id\":\"{userId}\"",
            $"\"passwprd\":\"{password}\""
        };
```