WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

アクションメソッドと構造不定の JSON (MVC5)

by WebSurfer 25. August 2021 11:33

ASP.NET MVC5 / Web API 2 のアクションメソッドで、構造が不定の JSON 文字列をどのように受け取って処理できるかという話を書きます。先の Core 版の記事「アクションメソッドと構造不定の JSON (CORE)」の .NET Framework 版です。

JSON 文字列から指定した name の value を取得

.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 文字列をそのまま送信しています。

Tags: , , , ,

MVC

アクションメソッドと構造不定の JSON (CORE)

by WebSurfer 23. August 2021 18:54

クライアントから ASP.NET Core MVC や Web API のアクションメソッドに送信されてくる JSON 文字列の構造が不定の場合、どのように受け取って処理できるかという話を書きます。(.NET Core 3.x 以降の話です。.NET Framework および .NET Core 2.x 以前は未検証・未確認です)

JSON 文字列から指定した name の value を取得

(注: 以下は MVC のアクションメソッドを例に取って書いていますが、Web API のアクションメソッドでもモデルバインディングの関係は全く同じです)

クライアントから送信されてくる JSON 文字列の構造が常に同じなら、先の記事「JSON 文字列から C# のクラス定義生成」に書いたような手段で C# のクラス定義を生成し、それをアクションメソッドの引数に設定してやれば、フレームワーク組み込みのモデルバインダが JSON 文字列を C# のオブジェクトにデシリアライズしてバインドしてくれます。

しかし、JSON 文字列の構造が不定の場合は C# のクラス定義ができません。それでも ASP.NET のフレームワークがモデルバインディングしてくれるようにするにはどのようにしたらいいでしょう?

構造が不定とは言っても必要な情報のある JSON 文字列 {"name" : "value"} の name は事前に分かっているのであれば、それに該当する value はJsonElement オブジェクトとして取得できます(詳しくは先の記事「JSON 文字列から指定した name の value を取得」を見てください)。それで目的が果たせるのであればその方向で進めるのが良さそうです。

クライアントから送信されてきた構造不定の JSON 文字列を、アクションメソッドでどのように受け取って JsonElement オブジェクトにデシリアライズするかですが、それはフレームワーク組み込みのモデルバインダが自動的に行ってくれます。

具体例は下のサンプルコードの Full アクションメソッドを見てください。JsonElement 型の引数を設定するだけで、自動的にクライアントから送信されてきた JSON 文字列をデシリアライズしてバインドしてくれます。

using Microsoft.AspNetCore.Mvc;
using MvcCore5App4.Models;
using System.Text.Json;

namespace MvcCore5App4.Controllers
{
    public class JsonController : Controller
    {
        [HttpPost]
        public IActionResult Partial([FromBody] Rootobject postedObject)
        {
            JsonElement element = postedObject.Response.Result.ObjectArray;
            return Content(element.ToString());
        }

        [HttpPost]
        public IActionResult Full([FromBody] JsonElement postedObject)
        {
            JsonElement? element = FindElementByName(postedObject, "ObjectArray");

            string returnValue = (element != null) ?
                element.Value.ToString() : "ObjectArray は取得できません";

            return Content(returnValue);
        }


        private JsonElement? FindElementByName(JsonElement jelem, string name)
        {
            if (jelem.ValueKind == JsonValueKind.Object)
            {
                foreach (JsonProperty jprop in jelem.EnumerateObject())
                {
                    if (jprop.Name == name)
                    {
                        return jprop.Value;
                    }
                    else
                    {
                        JsonElement? retVal = FindElementByName(jprop.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jelem.ValueKind == JsonValueKind.Array)
            {
                foreach (JsonElement jelemInArray in jelem.EnumerateArray())
                {
                    JsonElement? retVal = FindElementByName(jelemInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

送信した JSON 文字列のサンプルは以下の通りです。先の記事「JSON 文字列から指定した name の value を取得」に書いたものと同じです。

{
  "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"]
}

JSON 文字列まるまる全部の構造が不定なわけではなく、特定の項目だけが不定の場合は別の方法があります。例えば、上の JSON 文字列の中で "ObjectArray" の value のみが不定だとします。

その場合は、上の JSON 文字列から Visual Studio の機能を利用して生成したクラス定義の中の ObjectArray プロパティの型を JsonElement に書き換えてやります。具体的には以下の通りです。

using System.Text.Json;

namespace MvcCore5App4.Models
{
    // Visual Studio を利用して JSON 文字列から生成したクラス定義
    public class Rootobject
    {
        public string Title { get; set; }
        public Response Response { get; set; }
        public string[] StringArray { get; set; }
    }

    public class Response
    {
        public int Version { get; set; }
        public string StatusCode { get; set; }
        public Result Result { get; set; }
        public Message Message { get; set; }
    }

    public class Result
    {
        public Profile Profile { get; set; }

        // ObjectArray の value の構造が不定ということで書き換え
        //public Objectarray[] ObjectArray { get; set; }
        public JsonElement ObjectArray { get; set; }
        
        public string lstEnrollment { get; set; }
    }

    public class Profile
    {
        public string UserName { get; set; }
        public bool IsMobileNumberVerified { get; set; }
        public object MobilePhoneNumber { get; set; }
    }

    // ObjectArray クラスの定義は不要なのでコメントアウト
    //public class Objectarray
    //{
    //    public int Code { get; set; }
    //    public string Description { get; set; }
    //}

    public class Message
    {
        public int Code { get; set; }
        public string Description { get; set; }
    }
}

上の Rootobject クラスをアクションメソッドの引数に設定してやれば、ObjectArray プロパティには JsonElement 型、それ以外は上の定義に指定した通りの型にデシリアライズしてくれます。

そのアクションメソッドの具体例は上のコントローラのサンプルコードの Partial アクションメソッドを見てください。アクションメソッドの実行結果が上の画像です。

Tags: , , , , ,

CORE

MVC に JSON をバインドするには [FromBody] が必要 (CORE)

by WebSurfer 12. April 2021 15:14

ASP.NET Core MVC のアクションメソッドに JSON 文字列をボディに含めて POST 送信する場合、アクションメソッドの引数に [FromBody] 属性を付与しないと、なぜかモデルバインディングに失敗するという話を書きます。(Core 3.1 と 5.0 で確認。1.x, 2.x. 6.0 は未検証・未確認です)

モデルバインディングに失敗

上の画像がその例で、アクションメソッドの引数には [FromBody] を付与していません。RequestJson は生成されて引数の requestJson に渡されますが、各プロパティには送信されてきた JSON 文字列の値が代入されていません。(各プロパティの型の既定値のまま)

引数の RequestJson クラスの定義は以下の通りです。

namespace MvcCore5App2.Models
{
    public class RequestJson
    {
        public string Method { get; set; }
        public bool MuteHttpExceptions { get; set; }
        public string ContentType { get; set; }
        public string Payload { get; set; }
    }
}

JSON 文字列の送信は以下の画像のように Fiddler の Composer を使いました。(注: RequestJson クラスの各プロパティ名と JSON の {"name";"value"} の "name" の大文字小文字が違いますがそこは関係ないです。先の記事「JsonSerializer の Camel Casing (CORE)」に書いたように、Core v3.x 以降で使われる System.Text.Json 名前空間の JsonSerializer クラスは、デフォルトではデシリアライズする際の大文字小文字を区別するのですが、アプリ内で区別しない設定にしているようです)

Fiddler の Composer で送信

.NET Framework 版の MVC5 では [FromBody] を付与しなくてもモデルバインディングされたのですが・・・

何故か ASP.NET Core MVC では [FromBody] を付与しないと、送信されてきたクエリ文字列、フォームボディ、ルートパラメータ、クッキー、要求ヘッダ、ファイルなどのどれから取得するかが分からず、モデルバインディング (JSON 文字列から値を取得して各プロパティに代入) できないということのようです。(想像です)

以下の画像のように、アクションメソッドの引数に [FromBody] を付与すればモデルバインディングに成功します。送信した JSON 文字列は上の Fiddler の Composer 画像のものと同じですが、JSON の value が RequestJson クラスの各プロパティに代入されてから引数の requestJson に渡されています。

モデルバインディングに成功

上にも書きましたが、.NET Framework 版の MVC では [FromBody] 無しでも問���なくモデルバインディングされます。Core 版 MVC でも同じだろうと思っているとハマります。何を隠そう自分もハマって 2 時間ぐらい悩みました。

さらに、Core 版でも Web API の場合は [FromBody] 無しでも問題なくモデルバインディングされます。以下の画像を見てください。

Web API の場合

Web API では、コンプレックス型(この記事の例では RequestJson クラス)の場合、デフォルトではボディからパラメータを取得するということになるそうで、それゆえ [FromBody] 無しでも問題ないということかもしれません(想像です)。

Web API にせよ MVC5 にせよ、JSON 文字列をボディに含めて POST 送信するなら、それを受けるアクションメソッドの引数には [FromBody] を付与しておくというのが正解と思いました。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2021  >>
MoTuWeThFrSaSu
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar