WebSurfer's Home

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

JSON 文字列から C# のクラス定義生成

by WebSurfer 2020年5月10日 14:24

Newtonsoft.Json などを利用して JSON 文字列を C# のオブジェクトにデシリアライズする際、C# のオブジェクトのクラス定義が必要ですが、Visual Studio を利用してクラス定義を得ることが可能です。

JSON 文字列から C# のクラス定義生成

JSON 文字列をコピー(クリップボードに取得)し、Visual Studio のエディタ上でコードを張り付けたい場所にカーソルを置き、上の画像のように[編集(E)]⇒[形式を選択して貼り付け(S)]⇒[JSON をクラスとして張り付ける(J)]でカーソルの場所に C# のクラス定義が生成されます。

(VS2019 と VS2015 で確認。ちなみに、英語版 Visual Studio では [Edit] ⇒ [Paste Special] ⇒ [Paste JSON As Classes] となります)

具体的には、例えばある Web API の仕様として以下の JSON 文字列のサンプルが公開されていたとします。

{    
  "request":
  {
    "transaction":
    {
      "id" : "10",
      "server" : "10"
    },
    "list" :
    [
      { "group" : "1", "data" : "XXXXX" },
      { "group" : "1", "data" : "XXXXX" }
    ]
  }
}

C# のアプリケーションでこの Web API にアクセスして JSON 文字列を取得して利用する場合は、その JSON 文字列を C# のオブジェクトにデシリアライズすることになります。

デシリアライズに、例えば Newtonsoft.Json の DeserializeObject<T>(String) メソッドを使う場合 T のクラス定義が必要になります。

その場合、上の JSON 文字列をコピー(クリップボードに取得)して、この記事の一番上の画像のように Visual Studio を操作すれば以下の画像の通りカーソルを置いた場所にクラス定義が生成されます。

生成されたクラス定義

DeserializeObject<T>(String) メソッドの T は上の画像でいうと Rootobject になりますので、それを使って Rootobject 型の C# のオブジェクトにデシリアライズできます。

知ってました? 実は自分は最近まで知らなかったです。(汗)


【2023/12/24 追記】

VB.NET の場合ですが、この記事の機能を使って生成したクラス定義ではデシリアライズできないケースがあったので、忘れないように書いておきます。

例えば、上に書いた Visual Studio の機能を使って、以下の JSON 文字列から、

{
  "Response": [
    {
      "Version": 1,
      "StatusCode": "OK",
      "Data": [ "12", "34", "56" ]
    },
    {
      "Version": 2,
      "StatusCode": "NG",
      "Data": [ "78", "90", "02" ]
    }
  ]
}

VB.NET のクラス定義を生成すると以下のようになります。

Public Class Rootobject
    Public Property Response() As Response
End Class

Public Class Response
    Public Property Version As Integer
    Public Property StatusCode As String
    Public Property Data() As String
End Class

問題は Rootobject クラスの Response プロパティと Response クラスの Data プロパティの定義です。

上の JSON 文字列を System.Test.Json または Newtonsoft.json を使って、上の Rootobject, Response クラスのオブジェクトにデシリアライズしようとすると失敗して例外がスローされます。

問題を回避するには Response プロパティ、Data プロパティの定義を以下のように変更します。

Public Class Rootobject
    Public Property Response As Response()  ' 変更
End Class

Public Class Response
    Public Property Version As Integer
    Public Property StatusCode As String
    Public Property Data As String()  ' 変更
End Class

VB.NET の配列の宣言には以下のように 2 つの方法があって、前者の方が一般的なのか、この記事のツールも前者になります。また、配列の宣言と同時に割り当てを行う場合には前者のような書き方しかできないので、前者に統一しておくのがよいという記事も目にします。

Dim arrayA() As String
Dim arrayB As String()

しかしながら、デシリアライズに使うクラス定義のプロパティでは、上のコードで検証した限りの話ですが、配列の宣言は後者のようにする必要があるということでした。

Tags: , ,

DevelopmentTools

ASP.NET Core MVC の Unicode Escape Sequence (UES)

by WebSurfer 2020年3月11日 14:40

ASP.NET Core 3.1 MVC の Controller.Json メソッドを使って .NET オブジェクトを JSON 文字列にシリアライズすると日本語の文字は Unicode Escape Sequence (以下 UES と書きます) という形にエスケープされます。

JSON 文字列

UES というのは \uxxxx という形で表される Unicode 文字で、xxxx はその文字の Unicode コードになります。以下に、UES になる理由と、UES ではなく UTF-8 で(即ち、エスケープしないで)JSON 文字列に出力する方法を書きます。

UES となる理由はシリアライザでエスケープ処理が行われているからのようで、日本語の文字に限らず非 ASCII 文字は全てデフォルトで UES になるそうです。そのことは Microsoft のドキュメント How to serialize and deserialize (marshal and unmarshal) JSON in .NET に書いてありました。(注: そのドキュメントに書いてありますが、ASCII 文字でも HTML-sensitive characters はエスケープされます。例えば、< とか > はそれぞれ \u003C および \u003E になります)

ちなみに ASP.NET Core 3.1 Web API では UES にはならず、日本語の文字も UTF-8 で出力されます。何故 MVC と Web API で違う結果になるのかは不明です。(たぶん、MVC では HtmlEncode、JavaScriptEncode、UrlEncode の 3 つのエンコーダーすべてでエスケープされるようになっている、Web API では以下のサンプルコードのように BMP の文字はエスケープ対象から外す設定になっているからではなかろうかと想像しています)

日本語の文字も UES ではなく UTF-8 で出力する、即ちエスケープされないように設定するにはどうするかを以下に書きます。

Controller.Json メソッドには第 2 引数に JsonSerializerOptions クラスを設定���るオーバーロードがありますが、その JsonSerializerOptions.Encoder プロパティでエスケープ対象から外す文字の設定が可能なようです。

アクションメソッド単位で JsonSerializerOptions を設定する方法は Microsoft のドキュメント Configure System.Text.Json-based formatters を参照してください。具体的には以下のサンプルコードのように設定します。

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using WebAPI.Data;
using Microsoft.EntityFrameworkCore;

// エスケープ回避を設定するため追加
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;

namespace WebAPI.Controllers
{
  public class HomeController : Controller
  {
    private readonly BloggingContext _context;

    public HomeController(BloggingContext context)
    {
      _context = context;
    }

    public async Task<IActionResult> Json()
    {
      // Core は遅延ローディングが働かないので注意
      var list = await _context.Blogs.
                       Include(b => b.Posts).ToListAsync();

      // BMP 全てをエスケープしないよう設定
      // (WriteIndented はオマケ)
      return Json(list, new JsonSerializerOptions
      {
        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
        WriteIndented = true,
      });
    }
  }
}

上記のようなアクションメソッド単位でなく、Startup.cs でプロジェクト全体に設定することもできます。それには、AddControllersWithViews メソッドに以下のように AddJsonOptions メソッドを適用してやります。

services.AddControllersWithViews().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.WriteIndented = true;
    options.JsonSerializerOptions.Encoder = 
        JavaScriptEncoder.Create(UnicodeRanges.All);
});

上のコードでは、JavaScriptEncoder.Create(UnicodeRange[]) メソッドの引数に UnicodeRanges クラスの All プロパティを渡して BMP(Basic Multilingual Plane・・・U+0000 から U+FFFF の範囲)の文字をエスケープ対象から外すように設定しています。(WriteIndented プロパティの設定は UES とは関係ありません。これにより JSON 文字列がインデントされ見やすくなるので追加しました)

結果は以下のようになります。

JSON 文字列

なお、UnicodeRanges クラスの説明では、"現時点では、UnicodeRange クラスでサポートされているのは、基本多言語面 (BMP) の名前付き範囲のみです" とのことですので注意してください。

上のサンプルコードのように BMP 全てをエスケープしないよう設定しても、例えば 𠀋 という文字 (u2000b) は BMP にありませんが、それを JSON にシリアライズすると、 \uD840\uDC0B (サロゲートペアの形) になります。Web API でも同じです。

さらに、BMP の範囲内の文字でも全角空白 (U+3000) はエスケープされます。理由は GitHub の記事 Can't serializ the '\u3000' when using with UnicodeRanges.All に書いてありますが、U+3000 のみならず Space_Separator [Zs] category に属する文字は U+0020 (半角空白) 以外はブロックされる仕様になっているからだそうです。ブロックする理由は "their potential to cause problems or errors in consuming applications." だそうです・・・が、JSON 文字列の一部に過ぎないのにエスケープすると何故 "problems or errors" が避けられるのか納得できませんけど。何か別の使い方を考慮しているのかもしれません。


【2023/12/23 追記】

<. >, & などの HTML-sensitive 文字や + など文字は、上のサンプルコードのように BMP 全てをエスケープしないよう設定しても、やはりエスケープされます。

それらの文字もエスケープされないようにするには JavaScriptEncoder.UnsafeRelaxedJsonEscaping を使います。

ただし、Microsoft のドキュメント「すべての文字のシリアル化」に書いてあるように、セキュリティの問題がありますので注意が必要です。

なお、上に書いた全角空白 (U+3000) は、JavaScriptEncoder.UnsafeRelaxedJsonEscaping を使っても、やはりエスケープされます。

Tags: , , , , ,

CORE

JSON シリアライズの際の循環参照エラー

by WebSurfer 2020年3月8日 15:51

.NET Framework の ASP.NET MVC アプリや Web API アプリでオブジェクトを JSON 文字列にシリアライズするときの循環参照エラーの問題とその回避方法を書きます。

循環参照エラー

例えば、Entity Framework 6 の Code First 機能を利用して、以下のコード(Microsoft のチュートリアル「新しいデータベースへの Code First」のサンプルコードです)からデーターベースを生成し、Linq to Entities で取得したデータを JSON にシリアライズするケースを考えます。

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

以下のコードのように、Linq to Enitities を使ってデータベースからデータを取得し、ASP.NET MVC5 の Controller.Json メソッドを使って JSON 文字列にシリアライズしてみます。

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc5App.DAL;
using System.Data.Entity;

namespace Mvc5App.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Json()
        {
            var db = new BloggingContext();
            var list = db.Blogs.Include(b => b.Posts);
            return Json(list, JsonRequestBehavior.AllowGet);
        }
    }
}

上のアクションメソッド Json を呼び出すと、この記事の上の画像の通り、循環参照エラーになります。

(注: ちなみに、上のコードを var list = db.Blogs; に変えるとシリアライズの際に遅延ローディングが起こって "この Command に関連付けられている DataReader が既に開かれています。このコマンドを最初に閉じる必要があります" というエラーになります。var list = db.Blogs.ToList(); とすると、シリアライズの際に遅延ローディングが起こるのは同じですが、その時は DataReader が閉じているので循環参照のところまで進んで、上の画像と同じエラーになります)

ASP.NET Web API でも同様で、return db.Blogs.Include(b => b.Posts); とするとシリアライズする際に循環参照が生じて JSON へのシリアライズに失敗します。

原因は Post クラスにナビゲーションプロパティ Blog が定義されているためで、それをシリアライズしようとすると循環参照が発生し InvalidOperationException がスローされるからだそうです。ググってヒットした記事「ASP.NET Web API で循環参照なモデルの公開を解決する」を参考にさせていただきました。

その記事に書いてありますが、.NET Framework の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute Class という属性が利用できます。なので、上の Post クラスの Blog プロパティに [JsonIgnore] を付与すればシリアライズの際の循環参照エラーは回避できます。

しかしながら、MVC5 アプリで利用する Controller.Json メソッドは内部で JavaScriptSerializer クラスを使っており、JsonIgnore 属性は効果がありません。

なので、Microsoft のドキュメント「Create Data Transfer Objects (DTOs)」を参考にして以下のコードのようにしてみました。

public class BlogDto
{
    public int BlogId { get; set; }
    public string Name { get; set; }
    public List<PostDto> Posts { get; set; }
}

public class PostDto
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
}

public ActionResult Json()
{
    var db = new BloggingContext();

    var list = db.Blogs.Include(b => b.Posts).
               Select(b => new BlogDto
               {
                   BlogId = b.BlogId,
                   Name = b.Name,
                   Posts = b.Posts.Select(p => new PostDto
                   {
                       PostId = p.PostId,
                       Title = p.Title,
                       Content = p.Content,
                       BlogId = p.BlogId
                   }).ToList()
               });

    return Json(list, JsonRequestBehavior.AllowGet);
}

ナビゲーションプロパティを除いた別の入れ物(上の BlogDto クラス、PostDto クラス)に入れてシリアライズしているわけですから、当然ながら循環参照の問題はなくなって JSON 文字列にシリアライズできます。

循環参照を避ける以外に、必要なデータだけを望む形で JSON 文字列にシリアライズするということも出来るわけですから、積極的に DTO を使うのが正解だと思います。

なお、.NET Core 3.0 以降には JsonIgnoreAttribute クラスが利用できるので、上の .NET Framework のケースとは話は違ってきて、MVC, Web API どちらの場合も循環参照になるプロパティに [JsonIgnore] を付与すればシリアライズできます。

(Microsoft のドキュメント Add Newtonsoft.Json-based JSON format support を見ると ASP.NET Core 2.x 以前は Newtonsoft.Json パッケージが、ASP.NET Core 3.x 以降は System.Text.Json が使われているそうです)

Tags: , , ,

MVC

About this blog

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

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar