WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

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

by WebSurfer 11. February 2021 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

System.Text.Json の JsonElement をパース (CORE)

by WebSurfer 8. February 2021 13:03

.NET Core 3.0 以降で利用できる JsonSerializer クラスDeserialize<TValue>(String, JsonSerializerOptions) メソッドを使って JSON 文字列を JsonElement 構造体のオブジェクトにデシリアライズし、それをパースして下の画像のように表示する方法を書きます。

JsonElement をパース

基本的には先の記事「Json.NET の JToken をパース」と同じことを行っています。違うのは先の記事では JToken クラスにデシリアライズしていたものが JsonElement 構造体になったところです。

それに伴い、先の記事では JToken が JObject, JArray, JValue のどれにキャストできるかによってオブジェクトなのか配列なのかプリミティブ値なのかを調べてそれぞれ処理を分けていたところが、この記事の JsonElement の場合は JsonElement.ValueKind プロパティを使って JsonValueKind 列挙型のどれに該当するかを調べて処理を分けることになります。

また、foreach ループでの反復処理を、Object が対象の場合は JsonElement.EnumerateObject メソッドを使って、Array が対象の場合は JsonElement.EnumerateArray メソッドを使って列挙子を取得して行うところも先の記事とは異なります。

そのあたりは以下のサンプルコードのコメントに書きましたので見てください。

まず、Deserialize<JsonElement>(jsonText) で JSON 文字列を JsonElement 型のオブジェクトにデシリアライズします。その後 Parse メソッドで JsonElement オブジェクトの中身がプリミティブ型なのか Object なのか Array なのかを再帰的に解析し結果をコンソールに出力したのが上の画像です。

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\...\ConsoleAppJson\";
            string file = "TextFile5.txt";
            string jsonText = "";

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

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

        private static void Parse(int padding, JsonElement jelem)
        {
            // JsonElement.ValueKind プロパティを使って JsonValueKind 列挙型
            // のどれに該当するかを調べて処理を分ける
            if (jelem.ValueKind == JsonValueKind.False ||
                jelem.ValueKind == JsonValueKind.Null ||
                jelem.ValueKind == JsonValueKind.Number ||
                jelem.ValueKind == JsonValueKind.String ||
                jelem.ValueKind == JsonValueKind.True)
            {
                string str = $"value = {jelem}";
                Console.WriteLine(str.PadLeft(str.Length + padding));
            }
            else if (jelem.ValueKind == JsonValueKind.Object)
            {
                // Object の場合は EnumerateObject() で列挙子を取得し、以下の
                // ように foreach ループで JsonProperty を取得できる。
                // JsonProperty というのは単一の {"name":"value"} オブジェクト
                // と思えばよさそう
                foreach (JsonProperty jprop in jelem.EnumerateObject())
                {
                    if (jprop.Value.ValueKind == JsonValueKind.False ||
                        jprop.Value.ValueKind == JsonValueKind.Null ||
                        jprop.Value.ValueKind == JsonValueKind.Number ||
                        jprop.Value.ValueKind == JsonValueKind.String ||
                        jprop.Value.ValueKind == JsonValueKind.True)
                    {
                        string str = $"name = {jprop.Name}, value = {jprop.Value}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                    }
                    else if (jprop.Value.ValueKind == JsonValueKind.Object)
                    {
                        string str = $"name = {jprop.Name}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        Parse(padding + 2, jprop.Value);
                    }
                    else if (jprop.Value.ValueKind == JsonValueKind.Array)
                    {
                        string str = $"name = {jprop.Name}";
                        Console.WriteLine(str.PadLeft(str.Length + padding));
                        int index = 1;
                        // Array の場合は EnumerateArray() で列挙子を取得し、以下のよう
                        // に foreach ループで配列内の各要素 (JsonElement) を取得できる
                        foreach (JsonElement jelemInArray in jprop.Value.EnumerateArray())
                        {
                            string idx = $"array index {index}";
                            Console.WriteLine(idx.PadLeft(idx.Length + padding + 1));
                            Parse(padding + 2, jelemInArray);
                            index++;
                        }
                    }
                    else
                    {
                        // JsonValueKind.Undefined 以外はここに来ない(はず)
                        Console.WriteLine(jelem.ToString());
                    }
                }
            }
            else if (jelem.ValueKind == JsonValueKind.Array)
            {
                int index = 1;
                // Array の場合 EnumerateArray() で列挙子を取得し、以下のよう
                // に foreach ループで配列内の各要素 (JsonElement) を取得できる
                foreach (JsonElement jelemInArray in jelem.EnumerateArray())
                {
                    string idx = $"array index {index}";
                    Console.WriteLine(idx.PadLeft(idx.Length + padding + 1));
                    Parse(padding + 2, jelemInArray);
                    index++;
                }
            }
            else
            {
                // JsonValueKind.Undefined 以外はここに来ない(はず)
                Console.WriteLine(jelem.ToString());
            }
        }
    }
}

上の画像を出力するのに使った 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: , , , ,

CORE

About this blog

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

Calendar

<<  May 2021  >>
MoTuWeThFrSaSu
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar