詰まったこと
Unityクライアントアプリから、Laravelで開発しているサーバサイドへjson文字列を送る処理で
WWWFormにJsonUtilityでJSON文字にしたデータを送ろうとしたとき、サーバで受け取る時には抜け落ちていた。
原因と対応策についての備忘録。
#事象発生時の実装
スキルの効果量を計算するため、パーティメンバの現在のステータス、敵のステータス、補正を掛けるときに使う数値等々をクラスで持っており、JsonUtilityで文字列を生成する。
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にセット
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側に出ていたデバッグログ
~~ヘッダ情報等など~~
"x-unity-version":["2022.1.20f1"],
"content-length":["10932"]},
"body":[]} ※空!
生成したJSON文字をPostmanでBodyにセットした場合はしっかり出ている。
"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にはセットされず…
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にセットする。
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);
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);
}
WWWFormでうまく渡す方法も探します…。
補足
JsonUtilityの代わりにJsonConvert.SerializeObjectでエスケープ付の文字列生成できるようです。
エラーが出て確認できなかったので、機会あれば追記します。
# 追記(2023/8/13)
修正後の例ではjsonをそのまま渡していますが、key:valueのvalueが単純な文字列の場合
「"」をエスケープして囲む必要があります。忘れないようにメモ。
List<string> formDatas = new List<string>()
{
$"\"id\":\"{userId}\"",
$"\"passwprd\":\"{password}\""
};