WebSurfer's Home

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

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

by WebSurfer 2022年2月18日 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
*/

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

日付と通貨の書式設定

by WebSurfer 2019年2月4日 18:10

ASP.NET MVC5 アプリで日付(DateTime 型)と通貨(Decimal 型)を表示し、編集してデータベースを更新する場合、どのように書式設定を行うのが良いかということについて書きます。

日付と通貨の書式設定

上の画像は DisplayFor と TextBoxFor を上下に並べて表示していますが、その「契約日」と「価格」に示したように、

  1. DisplayFor 等を使った表示の場合のみ書式設定を行い、
  2. テキストボックスに値を表示する場合は書式設定しない。または、"{0:F0}" のような書式設定に留めパースできない文字は含めないようにする。

・・・のがよさそうです。

スキャフォールディング機能を使って CRUD 可能なアプリを作ると Index (レコード一覧), Create, Delete, Edit, Details というアクションメソッドとそれに対応するビューが生成されますが、Index, Delete, Details を上記 (1) で、Create, Edit を上記 (2) で表示するということです。

テキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のように表示することはできますが、クライアント側での検証を無効にしてサーバーに POST したとしても、DateTime 型や Decimal 型にパースできないので結局モデルバインディングの際の検証が通りません。

どうしてもテキストボックスに表示する文字列も 2019年2月4日 とか ¥1,000 のようにしたいということであれば、モデルのプロパティも含めて全て String 型として扱い、検証は正規表現のみを使って行うということも考えられますが、データベースの型が datetime とか money でしょうから、サーバー側での取り扱いを考えると現実的ではなさそうです。

それに、ユーザーとしても、編集する際にいちいち ¥ とか , を入力するのは煩わしいはずで、喜んではくれないのでは?

上の画像のように表示するための方法ですが、DisplayFor での表示の書式はモデルのプロパティに DisplayFormatAttribute を付与して設定し、テキストボックスには TextBoxFor を使ってその第 2 引数に "{0:yyyy/MM/dd}"、"{0:F0}" などの書式を設定するのがよさそうです。

EditorFor を使うと書式が自由に設定できないし(DisplayFormatAttribute の設定と同じでよければ可能ですが)、レンダリングされる html ソースの input 要素の type 属性が問題になることがありますので使う場合は要注意です。

具体的には以下のようにします。

Model

public class PurchaseRecord
{
  [Display(Name = "ID")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  public int ID { get; set; }

  [Display(Name = "契約日")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(
      @"^\d{4}/\d{2}/\d{2}( \d{1,2}:\d{2}:\d{2})?$", 
      ErrorMessage = "yyyy/MM/dd 形式")]
  [DisplayFormat(DataFormatString = "{0:yyyy年M月d日}")]
  public DateTime ContractDate { get; set; }

  [Display(Name = "価格")]
  [Required(ErrorMessage = "{0} は必須")]
  [RegularExpression(@"^\d{1,5}$", 
      ErrorMessage = "数字 1 ~ 5 文字")]
  [DisplayFormat(DataFormatString = "{0:C0}")]
  public decimal Price { get; set; }
}

View(一部の抜粋)

<div class="form-group">
  @Html.LabelFor(model => model.ID, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ID)
    @Html.EditorFor(model => model.ID, 
      new { htmlAttributes = new { @class = "form-control" } })
    @Html.ValidationMessageFor(model => model.ID, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.ContractDate, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.ContractDate)
    @Html.TextBoxFor(model => model.ContractDate, 
      "{0:yyyy/MM/dd}", new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.ContractDate, 
      "", new { @class = "text-danger" })
  </div>
</div>

<div class="form-group">
  @Html.LabelFor(model => model.Price, 
    htmlAttributes: new { @class = "control-label col-md-2" })
  <div class="col-md-10">
    @Html.DisplayFor(model => model.Price)
    @Html.TextBoxFor(model => model.Price, 
      "{0:F0}",new { @class = "form-control" })
    @Html.ValidationMessageFor(model => model.Price, 
      "", new { @class = "text-danger" })
  </div>
</div>

あと、オマケの話ですが、ASP.NET Web Forms アプリで使う GridView などで書式設定する際も同様なことはできます。以下の画像を見てください。

GridView で日付と通貨の書式設定

赤枠で囲った部分は[編集]ボタンをクリックして編集モードにしたテキストボックスですが、他の行との違いを見てください。

Tags: , ,

MVC

日付時刻と JSON 文字列

by WebSurfer 2017年8月27日 15:01

JavaScript の Date オブジェクトや .NET Framework の DateTime オブジェクトの JSON 文字列への変換について調べましたので備忘録として書いておきます。(逆の、下の表の JSON 文字列を Newtonsoft.Json および System.Text.Json 名前空間の JsonSerializer クラスを使って DateTime オブジェクトにデシリアライズする話は別の記事「JSON 文字列の日付時刻のデシリアライズ」に書きましたので興味があれば見てください)

ASP.NET ajax によるパース結果

JSON の紹介よると、その記事に書いてある value に直接設定できるのは string, number, object, array, true, false, null だけということですので、JavaScript の Date オブジェクトや .NET Framework の DateTime オブジェクトは string に変換して JSON 文字列に設定せざるを得ません。

変換は自力で行わなくても、例えば、JavaScript なら JSON.stringify() メソッド、.NET Framework なら DataContractJsonSerializer クラスを利用すれば自動的に変換してくれます。

その他、JavaScriptSerializer クラス、ASP.NET Web サービス、WCF、ASP.NET Web API でも DateTime 型を自動的に文字列に変換してくれますが、それぞれどのような結果になるかを以下にまとめておきます。

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

(注): Newtonsoft.Json と同じ。.NET Framework 版および Core v2.x 版の ASP.NET Web API はシリアライザに Newtonsoft.Json を使っています。ASP.NET Core 3.x 以降の Web API は System.Text.Json 名前空間JsonSerializer クラスがシリアライズに用いられるようになりました。なお、シリアライズの結果は (6) と同じになります。

以下に上記 (1) ~ (6) のそれぞれの検証方法や備忘録として残しておいた方がよさそうな情報を書いておきます。

(1) JSON.stringify() メソッド

ECMAScript 2017 Language Specification (ECMA-262, 8th edition, June 2017) の 20.3.1.16 Date Time String Format のセクションによると、ISO 8601 Extended Format 即ち:

YYYY-MM-DDTHH:mm:ss.sssZ

という形式に変換することになっているそうです。

全てのブラウザで確認したわけではありませんが、IE11, Chrome 60.0.3112.113, Firefox 55.0.3 では確かにその通りになるのを確認しました。

なお、JavaScript の Date オブジェクトはローカルタイムとして扱われまずが、JSON.stringify() メソッドで文字列に変換すると UTC になるので注意してください(YYYY-MM-DDTHH:mm:ss.sssZ の末尾の Z は UTC であることを意味します)。

例えば、以下のコードを実行すると、

var dateTime = new Date(2017, 1, 1, 12, 15, 45);
var dateObject = { DateTime: dateTime };
var dateJson = JSON.stringify(dateObject);

dateJson は {"DateTime":"2017-02-01T03:15:45.000Z"} という JSON 文字列になります。

Date コンストラクタの引数で月を表す整数値は 0 (1月) から 11 (12月) なので、1 ⇒ 02 という結果になっています。時間 12 が 03 になるのは東京ローカルタイムが UTC に変換されるためです。

(2) DataContractJsonSerializer クラス

DataContractJsonSerializer クラスを使うと、別の記事「w2ui Grid」に書きましたように、.NET Framework の DateTime 型は:

\/Date(1503727168573+0900)\/

のような文字列に変換されます。

この例の最初の数字 1503727168573 は UTC 1970 年 1 月 1 日午前 0 時からカウントしたミリ秒、+0900 はオプションで、ローカルタイムの UTC からの時差だそうです。

詳しくは以下の MSDN ライブラリの記事「スタンドアロン JSON のシリアル化」の「高度な情報 / DateTime ワイヤ形式」のセクションの説明を見てください。

(3) WCF

先の記事「WCF と jQuery AJAX」の CarService.svc.cs のコードに以下のメソッドを追加して試してみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace WebApplication1
{
    // web.config で aspNetCompatibilityEnabled="true" と設定され
    // ているので [AspNetCompatibilityRequirements(...)] が必要
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class CarService
    {
        // ・・・中略・・・

        [OperationContract]
        [WebGet(ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        public DateTimeJsonTest GetToday()
        {
            DateTimeJsonTest today = new DateTimeJsonTest();
            today.Today = DateTime.Now;
            return today;
        }
    }

    // ・・・中略・・・

    [DataContract]
    public class DateTimeJsonTest
    {
        [DataMember]
        public DateTime Today { set; get; }
    }
}

ブラウザ から上記コードの GetToday を要求すると以下のような JSON 文字列が返されます:

{"GetTodayResult":{"Today":"\/Date(1503727168573+0900)\/"}}

すなわち、上記「(2) DataContractJsonSerializer クラス」の場合と同じになります。(内部的に DataContractJsonSerializer クラスを使って変換しているのであろうと思います。未確認ですが・・・)

(4) JavaScriptSerializer クラス

ASP.NET MVC の Controller クラスの Json メソッドの内部で .NET Framework のオブジェクトを JSON 文字列にシリアライズするために JavaScriptSerializer クラスが用いられているそうです。

先の記事「jQuery.ajax で JSONP」に JavaScriptSerializer クラスを使ってシリアル化を行っている例がありますので、これに少し手を加えて検証してみました。

その記事のコードを見てください。Controller の Jsonp アクションメソッドでは Address テーブルから AddressID, AddressLine1, City フィールドのデータを取得して匿名クラスのオブジェクトを作り、それを JavaScriptSerializer クラスの Serialize メソッドで JSON 文字列にシリアライズしています。

Address テーブルには ModifiedDate という datetime 型のフィールドがありますのでそれを匿名クラス address に追加してシリアライズしてみました。

Serialize(address) の結果は以下のような文字列となります。

{"AddressID":25,"AddressLine1":"9178 Jumping St.","City":"Dallas","ModifiedDate":"\/Date(1030806000000)\/"}

即ち、DateTime 型の変換結果は上記「(2) DataContractJsonSerializer クラス」とほぼ同じで、オプションのローカルタイムの UTC からの時差(上の例では +0900)が省略された形になっています。

(5) ASP.NET Web サービス

先の記事「ASP.NET AJAX と Web サービス」のコードに以下のメソッドを追加して試してみました。

<%@ WebService Language="C#" Class="CarService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Web.Script.Services;
using System.Collections.Generic;
using System.Linq;

// ・・・中略・・・

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class CarService : WebService
{
    // ・・・中略・・・

    [WebMethod]
    public DateTimeJsonTest GetToday()
    {
        DateTimeJsonTest today = new DateTimeJsonTest();
        today.Today = DateTime.Now;
        return today;
    }
}

public class DateTimeJsonTest
{
    public DateTime Today { set; get; }
}

上記 GetToday() メソッドを、要求ヘッダに Content-Type: application/json を追加して POST 要求すると(注)以下のような JSON 文字列が返されます:

{"d":{"__type":"DateTimeJsonTest","Today":"\/Date(1503712714770)\/"}}

(注)ASP.NET Web サービスの制約上 POST 要求しないとエラーになります。Content-Type を指定しない、または application/x-www-form-urlencoded とすると XML 形式で返されます。XML の場合は形式は上記とは異なり 2017-08-26T10:36:27.0809765+09:00 のようになります。

即ち、上記「(4) JavaScriptSerializer クラス」と同じ形式の文字列に変換されます(+0900 も同じく省略されています)。

ASP.NET AJAX を利用すると、\/Date(1503712714770)\/ という形式の文字列は、Date 型の JavaScript オブジェクトに自動的に変換されます。具体例は以下の通りです。

記事「ASP.NET AJAX と Web サービス」の「(2) JavaScript (097_ASPNETAjaxAndWebService.js)」のセクションに記載されているコードを以下のように書き換えます。

var serviceProxy;

// プロキシの初期化とコールバック関数の設定
function pageLoad() {
    serviceProxy = new CarService();
    serviceProxy.set_defaultSucceededCallback(Succeeded);
    serviceProxy.set_defaultFailedCallback(Failed);
}

// ボタンクリックで呼び出されるサービスメソッド  
function getToday() {
    serviceProxy.GetToday();
}

// AJAX 通信が成功したときに呼び出され、戻ってきたデータ
// を処置するコールバック関数。
// 引数 data は JSON 文字列ではなく、DateTimeJsonTest 型
// オブジェクト。それから Today プロパティで取得できるの
// は、文字列ではなく Date 型に変換済みのオブジェクト。
function Succeeded(data) {
    $('#output').empty();
    var now = data.Today;  // now は Date 型オブジェクト
    $('#output').append(
        "<p>" + now.getFullYear() + "年" +
        (now.getMonth() + 1) + "月" + now.getDate() + "日" +
        now.getHours() + "時" + now.getMinutes() + "分" +
        now.getSeconds() + "秒</p>"
    );
}

// ・・・以下略・・・

上の getToday() メソッドを「(3) apsx ページ (097_ASPNETAjaxAndWebService.aspx) 」のセクションに記載されているコードと同等な aspx ページから呼び出すと、Date 型オブジェクトが上記コードの now に取得でき、この記事の一番上の画像のとおり表示されます。

MSDN ライブラリの記事「スタンドアロン JSON のシリアル化」の「高度な情報 / DateTime ワイヤ形式」のセクションの説明によると、"UTC からの時差を示す部分(例: +0900)が指定されていない場合、時刻は UTC として逆シリアル化されます" とありますが、上記コードの結果はローカル時刻になります。

ASP.NET AJAX に代えて jQuery ajax を使う場合は自動的に Date オブジェクトには変換してくれませんので、自力でコードを書いて変換することになります。

具体例は以下の通りです。数字の部分を抜き出して JavaScript の Date コンストラクタの引数として渡し、Date オブジェクトを生成すれば ASP.NET AJAX と同じ結果が得られます。

function getDateByJqueryAjax() {
  $.ajax({
    type: "POST",

    url: "097_jQueryAjaxAndWebService.asmx/GetToday",

    contentType: "application/json; charset=utf-8",

    success: function (data) {
      if (data.hasOwnProperty('d')) {
          data = data.d;
      }
      $('#output').empty();
      var re = /^\/Date\(([0-9]+)\)\/$/;
      var results = re.exec(data.Today);
      var now = new Date(Number(results[1]));
      $('#output').append(
        "<p>" + now.getFullYear() + "年" +
        (now.getMonth() + 1) + "月" + now.getDate() + "日" +
        now.getHours() + "時" + now.getMinutes() + "分" +
        now.getSeconds() + "秒</p>"
      );
    },

    error: function (jqXHR, textStatus, errorThrown) {
      $('#output').text('textStatus: ' + textStatus +
                  ', errorThrown: ' + errorThrown);
    }
  });
}

(6) ASP.NET Web API (Newtonsoft.Json と同じ)

Visual Studio 2010 Professional のテンプレートを使って自動生成される ASP.NET Web API アプリのコードに、以下のメソッドを追加して試してみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using WebApi1.Models;

namespace WebApi1.Controllers
{
    public class HeroesController : ApiController
    {
        public DateTimeJsonTest Get()
        {
            DateTimeJsonTest today = new DateTimeJsonTest();
            today.Today = DateTime.Now;
            return today;
        }

        // ・・・中略・・・
    }
}

jQuery ajax を利用して上の Get() メソッドを呼び出すと以下のような JSON 文字列が返されます:

{"Today":"2017-08-26T15:39:32.6330349+09:00"}

DateTime 型の文字列への変換は上の「(5) ASP.NET Web サービス」の場合の XML 形式の応答の場合と同じです。

上にも書きましたが、ASP.NET Web API はシリアライザに Newtonsoft.Json を使っています。この形式は Newtonsoft.Json 独自なのか、ISO 8601 などに準拠しているのかは調べ切れてません。

なお、この形式で、JavaScript の Date 型へのパースは IE11, Chrome 60.0.3112.113, Firefox 55.0.3 のいずれも可能です。例えば、jQuery ajax を使うと、success オプションに設定したコールバックの引数に上の JSON 文字列を JavaScript オブジェクトに変換して代入してくれますので、それから以下のようにして Date オブジェクトにパースできます。

success: function (data, textStatus, jqXHR) {
    var now = new Date(data.Today);
    $('#heroes').append(
        "<p>" + now.getFullYear() + "年" +
        (now.getMonth() + 1) + "月" + now.getDate() + "日" +
        now.getHours() + "時" + now.getMinutes() + "分" +
        now.getSeconds() + "秒</p>"
    );
},

Tags: , ,

JavaScript

About this blog

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

Calendar

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

View posts in large calendar