WebSurfer's Home

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

Web API と要求ヘッダの application/xml

by WebSurfer 2020年6月7日 14:12

.NET Framework 版の ASP.NET Web API を要求する際、要求ヘッダの Accept に application/xml が含まれると応答は JSON ではなく XML になります。

ASP.NET Web API の応答

知ってました? 実は自分は知らなかったです。(汗) ちなみに、MVC の Json メソッド、および Core 3.1 Web API と MVC の Json メソッドは、要求ヘッダの Accept に application/xml が含まれていても JSON が返ってきます。

Web API の応答をチェックするには Postman などのツールを使うのが一般的なようです。でも、GET 要求ならブラウザのアドレスバーに直接 URL を入力して応答を見ることでも十分だったので自分は IE11 を使ってそうしてました。それで期待した通り JSON 文字列が返ってきてました。

ところが、Chrome, Edge, Firefox では応答が JSON ではなく XML になってしまいます。理由は要求ヘッダの Accept に application/xml が含まれるからのようです。ハマったのは上の画像のようなサーバーエラーとなってしまったことです(HTTP 500 応答が返ってきます。上の画像は形式は XML ですが中身はエラーメッセージです)。

エラーメッセージ "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'." によるとシリアライズに失敗したということです。

IE11 や jQuery ajax を使った検証ではシリアライズには問題はないことは確認しており、ブラウザに Chrome などを使ったからと言ってそれがシリアライズの部分に影響があるとは考えられなかったです。何故なのでしょう?

循環参照の問題でした。

上の画像の InnerException のエラーメッセージにある Blog_E36679EB... D5AD0 という文字列が、先の記事「JSON シリアライズの際の循環参照エラー」にもあったのを思い出して気が付きました。

.NET Framework 版の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute クラスという属性が利用できます。なので、問題のプロパティに [JsonIgnore] を付与してシリアライズの際の循環参照エラーは回避していました。

でも [JsonIgnore] が有効なのは JSON にシリアライズするときのみで、XML にシリアライズするときは循環参照の問題は回避できません。

Chrome, Edge などを使った時は、ASP.NET Web API は要求ヘッダの Accept に application/xml が含まれるのを見て、XML にシリアライズしようとして循環参照の問題に陥ったということのようです。

Tags: , , ,

Web API

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年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar