ASP.NET MVC5 / Web API 2 のアクションメソッドで、構造が不定の JSON 文字列をどのように受け取って処理できるかという話を書きます。先の Core 版の記事「アクションメソッドと構造不定の JSON (CORE)」の .NET Framework 版です。
.NET Framework / Core 2.x 以前と Core 3.x 以降の ASP.NET MVC / Web API では JSON デシリアライズに使うライブラリが異なり、前者はサードパーティ製の Newtonsoft.JSON、後者は System.Text.Json 名前空間のものになります。
デシリアライザが違っても、先の記事でアクションメソッドの引数に使った System.Text.Json 名前空間の JsonElement 構造体を Newtonsoft.JSON の JToken クラスに変えれば、モデルバインダが自動的にデシリアライズしてくれるのではないか��期待して試したのですが MVC はダメでした。
MVC のアクションメソッドの場合、先の記事のように Partial(Rootobject postedObject) とすると postedObject に渡される前の処理でエラーになります。Full(JToken postedObject) としても同じくエラーになります。
一方、Web API のアクションメソッドの場合は、先の Core 版の記事と同様にして期待通りの結果になりました。コードの違いはこの記事の下の方に書きましたので見てください。
MVC と Web API で結果が異なるのは、想像ですが、モデルバインドの仕組みが異なるためと思われます。先の記事「ASP.NET Web API のバインディング」に書きましたが、Web API の場合は Model Binding または Formatter を利用するという 2 つの方法があって、クエリ文字列からパラメータを取得する場合は Model Binding、ボディから取得する場合は Formatter を使うそうです。
なので、MVC の場合は、プリミティブな方法ですが、アクションメソッドの引数には JSON 文字列のまま渡して、アクションメソッドの中でデシリアライズするのが良さそうです。(他にカスタムモデルバインダを使うとかの手段があるかもしれませんが、余計に複雑になりそうな気がしましたので、途中で考えるのをやめました(笑))
検証に使った MVC の Controller / Action Method のコードは以下の通りです。
using System.Collections.Generic;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Mvc5WithWebAPI.Models;
namespace Mvc5WithWebAPI.Controllers
{
public class JsonModelbindingController : Controller
{
// Partial(Rootobject postedObject) ではエラー
[HttpPost]
public ActionResult Partial(string jsonText)
{
var postedObject = JsonConvert.DeserializeObject<Rootobject>(jsonText);
JToken element = postedObject.Response.Result.ObjectArray;
return Content(element.ToString());
}
// Full(JToken postedObject) ではエラー
[HttpPost]
public ActionResult Full(string jsonText)
{
JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);
JToken element = FindJTokenByName(jtoken, "ObjectArray");
string returnValue = (element != null) ?
element.ToString() : "ObjectArray は取得できません";
return Content(returnValue);
}
private static JToken FindJTokenByName(JToken jtoken, string name)
{
if (jtoken is JObject)
{
foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
{
if (kvp.Key == name)
{
return kvp.Value;
}
else
{
JToken retVal = FindJTokenByName(kvp.Value, name);
if (retVal != null)
{
return retVal;
}
}
}
}
else if (jtoken is JArray)
{
foreach (JToken jtokenInArray in (JArray)jtoken)
{
JToken retVal = FindJTokenByName(jtokenInArray, name);
if (retVal != null)
{
return retVal;
}
}
}
else
{
return null;
}
return null;
}
}
}
検証に使った JSON 文字列とそれをベースに作成した C# のクラス定義は、先の記事「アクションメソッドと構造不定の JSON (CORE)」のものと同じですので、そちらを見てください。
クライアント側は jQuery ajax を使って {"JsonText":"JSON 文字列"}という形で MVC のアクションメソッドに送信して検証しました。その応答をブラウザ上に表示したのが上の画像です。
検証に使った View のコードを以下に記載しておきます。コメントで「MVC 向け」とした部分を見てください。
@{
ViewBag.Title = "JsonModelbinding";
}
<h2>JsonModelbinding</h2>
<input id="button1" type="button" value="Web API" />
<input id="button2" type="button" value="MVC" />
<div id="result"></div>
@section Scripts {
<script type="text/javascript">
//<![CDATA[
var json =
'{ "Title": "This is my title",' +
'"Response": { "Version": 1, "StatusCode": "OK",' +
'"Result": { "Profile": { "UserName": "SampleUser", "IsMobileNumberVerified": false, "MobilePhoneNumber": null },' +
'"ObjectArray" : [{"Code": 2000,"Description": "Fail"},{"Code": 3000,"Description": "Success"}],' +
'"lstEnrollment": "2021-2-5"},' +
'"Message": {"Code": 1000, "Description": "OK"}},' +
'"StringArray" : ["abc", "def", "ghi"] }';
// Web API 向け
$("#button1").on("click", function () {
$.ajax({
type: "POST",
url: "/WebApi/Partial",
data: json,
contentType: "application/json; charset=utf-8",
}).done(function (data) {
$("#result").empty();
$("#result").append(data);
}).fail(function (jqXHR, status, error) {
$('#result').text('Status: ' + status +
', Error: ' + error);
});
});
// MVC 向け
$("#button2").on("click", function () {
var j = { JsonText: json };
var text = JSON.stringify(j);
$.ajax({
type: "POST",
url: "/JsonModelbinding/Partial",
data: text,
contentType: "application/json; charset=utf-8",
}).done(function (data) {
$("#result").empty();
$("#result").append(data);
}).fail(function (jqXHR, status, error) {
$('#result').text('Status: ' + status +
', Error: ' + error);
});
});
//]]>
</script>
}
Web API の場合は、上にも書きましたが、組み込みの Formatter が JSON 文字列をデシリアライズしてアクションメソッドの引数に渡してくれるからか、先の Core 版の記事と同様にして期待通りの結果が得られました。
検証に使った Web API の Controller / Action Method のコードは以下の通りです。
using System.Collections.Generic;
using System.Web.Http;
using Newtonsoft.Json.Linq;
using Mvc5WithWebAPI.Models;
namespace Mvc5WithWebAPI.Controllers
{
public class ValuesController : ApiController
{
[HttpPost]
[Route("WebApi/Partial")]
public string Partial([FromBody] Rootobject postedObject)
{
JToken element = postedObject.Response.Result.ObjectArray;
return element.ToString();
}
[HttpPost]
[Route("WebApi/Full")]
public string Full([FromBody] JToken postedObject)
{
JToken element = FindJTokenByName(postedObject, "ObjectArray");
string returnValue = (element != null) ?
element.ToString() : "ObjectArray は取得できません";
return returnValue;
}
private static JToken FindJTokenByName(JToken jtoken, string name)
{
if (jtoken is JObject)
{
foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
{
if (kvp.Key == name)
{
return kvp.Value;
}
else
{
JToken retVal = FindJTokenByName(kvp.Value, name);
if (retVal != null)
{
return retVal;
}
}
}
}
else if (jtoken is JArray)
{
foreach (JToken jtokenInArray in (JArray)jtoken)
{
JToken retVal = FindJTokenByName(jtokenInArray, name);
if (retVal != null)
{
return retVal;
}
}
}
else
{
return null;
}
return null;
}
}
}
クライアント側は、上の View のコードのコメントで「Web API 向け」とした部分を見てください。先の Core 版の記事と同様に、サンプル JSON 文字列をそのまま送信しています。