ASP.NET Core 3.1 MVC の Controller.Json メソッドを使って .NET オブジェクトを JSON 文字列にシリアライズすると日本語の文字は Unicode Escape Sequence (以下 UES と書きます) という形にエスケープされます。
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 文字列がインデントされ見やすくなるので追加しました)
結果は以下のようになります。
なお、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 を使っても、やはりエスケープされます。