WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

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

by WebSurfer 2021年8月25日 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 文字列から指定した name の value を取得

by WebSurfer 2021年2月11日 18:12

JSON 形式の文字列 {"name":"value"} から name を指定して value を取得する C# のプログラムのサンプルを書きます。

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

先の記事「Json.NET の JToken をパース」と「System.Text.Json の JsonElement をパース」で、Json.NET (Newtonsoft.Json) と System.Text.Json を使って JSON 文字列をパースするサンプルを書きました。

先の記事のサンプルの使い道はなさそうですが、その応用でいろいろできそうということで、まず JSON 文字列を JToken (Json.NET) または JsonElement (System.Text.Json) にデシリアライズしたオブジェクトから、name を指定して再帰的に value 値を取得するコードを書いてみました。

それが何の役に立つかというと、例えば JSON 文字列構造が不定なので特定のクラスにデシリアライズできないが、必要な情報のある name は事前に分かっているのでそれに該当する value を取得できれば目的は果たせるというようなケースです。

レアケースかもしれませんけど、ASP.NET Forum でそういう話があったので考えてみました。

JToken (Json.NET) および JsonElement (System.Text.Json) の両方の場合のサンプルコードを以下にアップしておきます。

(1) JToken (Json.NET)

Json.NET (Newtonsoft.Json) の DeserializeObject メソッドで JSON 文字列を JToken クラスのオブジェクトにデシリアライズし、その中から name が "ObjectArray" の value 値を FindJTokenByName メソッドで取得しています。

その結果を Visual Studio 2019 のデバッガで見たのが上の画像です。元になる JSON 文字列はこの記事の下の方に書いてありますのでそちらを見てください。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;

namespace NewtonsoftJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\...\NewtonsoftJson\";
            string file = "TextFile5.txt";
            string jsonText = "";
            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);
            Parse(0, jtoken);
            JToken token = FindJTokenByName(jtoken, "ObjectArray");
        }

        private static void Parse(int padding, JToken jtoken)
        {
            // ・・・略・・・
        }

        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;
        }
    }
}

(2) JsonElement (System.Text.Json)

System.Text.Json の Deserialize メソッドで JSON 文字列を JsonElement 構造体のオブジェクトにデシリアライズし、その中から name が "ObjectArray" の value 値を FindElementByName メソッドで取得しています。

using System;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using System.IO;

namespace ConsoleAppJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\surfe...\ConsoleAppJson\";
            string file = "TextFile5.txt";
            string jsonText = "";

            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JsonElement jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonText);
            Parse(0, jsonElement);
            var jelem = FindElementByName(jsonElement, "ObjectArray");
        }

        private static void Parse(int padding, JsonElement jelem)
        {
            // ・・・略・・・
        }

        // JsonElement は構造体。見つからないと null が返ってくるので
        // 戻り値は JsonElement? として null 許容にした
        private static 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;
        }
    }
}

上のコードで使った TextFile5.txt の 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"]
}

Tags: , ,

.NET Framework

Json.NET の JToken をパース

by WebSurfer 2021年2月7日 15:04

Json.NET (Newtonsoft.Json) の JsonConvert.DeserializeObject メソッドを使って JSON 文字列を JToken 型のオブジェクトにデシリアライズし、それを以下のようにパースして表示する方法を書きます。

JToken をパース

使用するのは Newtonsoft.Json.Linq 名前空間にある JToken, JObject, JArray, JValue クラスですので、まずそれらの簡単な説明を以下に書きます。詳しい説明は Newtonsoft.Json.Linq Namespace を見てください。

  • JToken: 下のクラスの継承元の抽象クラス
  • JObject: {"name1":"value1","name2":"value2"} のようなオブジェクト。IDictionary<string, JToken> を継承
  • JArray: [{"name":"value1"},{"name":"value2"}, ...] のような配列。IList<JToken> を継承
  • JValue: string, number, boolean, null などのプリミティブな JSON 値

一般的な方法は DeserializeObject<T>(String) メソッドの T に先の記事「JSON 文字列から C# のクラス定義生成」のような方法で求めたクラス定義を設定して、そのクラスのオブジェクトにデシリアライズするのだと思います。

なので、この記事に書くような JToken クラスにデシリアライズしてそれをパースする必要はなさそうですが、ひょっとしたら JSON 文字列を受け取るまでその内容が不明でクラス定義が生成できないとかいうケースがあるかもしれないということで考えてみました。

そのコードを以下にアップしておきます。

DeserializeObject<JToken>(jsonText) で JSON 文字列を JToken 型のオブジェクトにデシリアライズします。その後 Parse メソッドで JToken オブジェクトを再帰的に解析し結果をコンソールに出力したのが上の画像です。

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;

namespace NewtonsoftJson
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"C:\Users\...\NewtonsoftJson\";
            string file = "TextFile5.txt";

            string jsonText = "";

            file = "TextFile5.txt";
            using (StreamReader sr = File.OpenText(path + file))
            {
                jsonText = sr.ReadToEnd();
            }

            JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);
            Parse(0, jtoken);

        }

        private static void Parse(int padding, JToken jtoken)
        {
            if (jtoken is JValue)
            {
                // プリミティブ型の場合 Value プロパティで値を取得
                JValue jvalue = (JValue)jtoken;
                string str = $"value = {jvalue.Value}";
                Console.WriteLine(str.PadLeft(str.Length + padding));
            }
            else if (jtoken is JObject)
            {
                // JObject は IDictionary<string, JToken> を継承しているので、
                // 以下のように foreach ループで {"name":"value"} を取得できる
                foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
                {
                    if (kvp.Value is JValue)
                    {
                        JValue jvalue = (JValue)kvp.Value;
                        string str = $"name = {kvp.Key}, value = {jvalue.Value}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                    }
                    else if (kvp.Value is JObject)
                    {
                        string str = $"name = {kvp.Key}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        Parse(padding + 2, kvp.Value);
                    }
                    else if (kvp.Value is JArray)
                    {
                        string str = $"name = {kvp.Key}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        JArray jarray = (JArray)kvp.Value;
                        int index = 1;

                        // JArray は IList<JToken> を継承しているので、以下の
                        // ように foreach で配列の各要素の JToken を取得できる
                        foreach (JToken token in jarray)
                        {
                            string idx = $"array index {index}";
                            Console.WriteLine(idx.PadLeft(idx.Length + padding + 1));
                            Parse(padding + 2, token);
                            index++;
                        }
                    }
                }
            }
            else if (jtoken is JArray)
            {
                JArray jarray = (JArray)jtoken;
                int index = 1;
                foreach (JToken token in jarray)
                {
                    string str = $"array index {index}";
                    Console.WriteLine(str.PadLeft(str.Length + padding + 1));
                    Parse(padding + 2, token);
                    index++;
                }
            }
            else
            {
                // ここには来ないはずだが念のため
                Console.WriteLine(jtoken.ToString());
            }
        }
    }
}

ちなみに、上のコードで使った JSON 文字列は以下の通りです。他にもいろいろ試してみましたが、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"]
}

Tags: , , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar