WebSurfer's Home

Filter by APML

JSON 文字列の日付時刻のデシリアライズ

by WebSurfer 18. February 2022 15:49

日付時刻を表す JSON 文字列を .NET の DateTime 型のオブジェクトにデシリアライズするにはどのようにするかという話を書きます。

{"name":"value"} という JSON 文字列の value に直接設定できるのは string, number, object, array, true, false, null だけですので、.NET の DateTime オブジェクトは string 型に変換されて JSON 文字列に設定されます。

変換の結果はシリアライザによって違います。詳しくは先の記事「日付時刻と JSON 文字列」に書きましたが、それをまとめた表を以下に再掲しておきます。表の一番最後の項目 (6) ASP.NET Web API は Newtonsoft.Json および System.Text.Json 名前空間の JsonSerializer クラスを使ったシリアライズ結果です。

No. 方法 結果
(1) JSON.stringify() 2017-02-01T03:15:45.000Z
(2) DataContractJsonSerializer \/Date(1503727168573+0900)\/
(3) WCF 上記 (2) の結果と同じ
(4) JavaScriptSerializer \/Date(1030806000000)\/
(5) ASP.NET Web サービス 上記 (4) の結果と同じ
(6) ASP.NET Web API 2017-08-26T15:39:32.6330349+09:00

上の表のように string 型にシリアライズされた JSON 文字列を元の DateTime 型のオブジェクトに変換するには、自力でコードを書いて文字列をパースする必要があると思っていました。

しかしそれは思い違いで、上の表の形式にシリアライズした文字列は Newtonsoft.Json であれば全て、System.Text.Json 名前空間の JsonSerializer クラスの場合も (1) と (6) は DateTime 型にデシリアライズしてくれました。

知ってましたか? 実は自分は最近まで知らなかったです。(汗) 以下にその話を書きます。

まず、JSON 文字列を C# のオブジェクトにデシリアライズする際、C# のオブジェクトのクラス定義が必要ですが、Visual Studio のツールを利用してそれが可能です。詳しくは先の記事「JSON 文字列から C# のクラス定義生成」を見てください。

Visual Studio のツールを使って C# のクラス定義を生成すると、上の表の日付日時の JSON 文字列に該当するプロパティの型は、上の表の例 (1) ~ (6) 全てが DateTime 型になります。

例えば、以下のような JSON 文字列から Visual Studio のツールを使って C# のクラス定義を生成すると、

{
  "name":"WebSurfer",
  "date":"2017-02-01T03:15:45.000Z"
}

日付時間の JSON 文字列に該当するプロパティの型は以下のよう DataTime 型になります。

public class Rootobject
{
    public string name { get; set; }
    public DateTime date { get; set; }
}

シリアライザによって JSON 文字列の形式が上の表の (1) ~ (6) のように異なりますが、いずれも同じ結果すなわち C# のクラスの当該プロパティの型は DateTime になります。

次にデシリアライザが上の表の (1) ~ (6) の文字列に対応しているかを調べました。結果は上にも述べましたが Newtonsoft.Json は (1) ~ (6) 全てに対応していました。検証に使ったコードを以下に載せておきます。

using Newtonsoft.Json;
using System;

namespace ConsoleNewtonsoftJson
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string jsonString1 = "{\"name\":\"WebSurfer\",\"date\":\"2017-02-01T03:15:45.000Z\"}";
            string jsonString2 = "{\"name\":\"WebSurfer\",\"date\":\"\\/Date(1503727168573+0900)\\/\"}";
            string jsonString3 = "{\"name\":\"WebSurfer\",\"date\":\"\\/Date(1030806000000)\\/\"}";
            string jsonString4 = "{\"name\":\"WebSurfer\",\"date\":\"2017-08-26T15:39:32.6330349+09:00\"}";

            Rootobject rootobject = JsonConvert.DeserializeObject<Rootobject>(jsonString1);
            Console.WriteLine($"name: {rootobject.name}, date: {rootobject.date}");

            rootobject = JsonConvert.DeserializeObject<Rootobject>(jsonString2);
            Console.WriteLine($"name: {rootobject.name}, date: {rootobject.date}");

            rootobject = JsonConvert.DeserializeObject<Rootobject>(jsonString3);
            Console.WriteLine($"name: {rootobject.name}, date: {rootobject.date}");

            rootobject = JsonConvert.DeserializeObject<Rootobject>(jsonString4);
            Console.WriteLine($"name: {rootobject.name}, date: {rootobject.date}");
        }
    }

    public class Rootobject
    {
        public string name { get; set; }
        public DateTime date { get; set; }
    }
}

/*
実行結果は:
name: WebSurfer, date: 2017/02/01 3:15:45
name: WebSurfer, date: 2017/08/26 14:59:28
name: WebSurfer, date: 2002/08/31 15:00:00
name: WebSurfer, date: 2017/08/26 15:39:32
*/

System.Text.Json 名前空間の JsonSerializer クラスのデシリアライザも同様に検証してみましたが、jsonString2 と jsonString3 場合は JsonException がスローされ "The JSON value could not be converted to System.DateTime." というエラーメッセージが出ます。jsonString1 と jsonString4 は問題なく DateTime 型のオブジェクトにデシリアライズされました。

例外のスタックトレースを見ると、Utf8JsonReader.GetDateTime() メソッドで例外がスローされており、そのメソッドの Microsoft のドキュメントを見ると Remarks に "This method only creates a DateTime representation of JSON strings that conform to the ISO 8601-1 extended format" と書いてあります。

ということで、上の表の (1) と (6) の文字列は ISO 8601-1 extended format に準拠しているので DateTime にデシリアライズできた、他の形式はダメだったということのようです。

Tags: , ,

.NET Framework

アクションメソッドと構造不定の 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

About this blog

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

Calendar

<<  December 2024  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar