NEXTSCAPE blog

株式会社ネクストスケープの社員による会社公式ブログです。ネスケラボでは、社員が日頃どのようなことに興味をもっているのか、仕事を通してどのような面白いことに取り組んでいるのかなど、会社や技術に関する情報をマイペースに紹介しています。

MENU

.NET 7 での System.Text.Json のシリアル化

こんにちは、ネクストスケープでエンジニアをしている醍醐です。

この記事は「NEXTSCAPE Advent Calendar 2022」の 19 日目です。

qiita.com

はじめに

先日.NET 7 がリリースされました。.NET のリリースサイクルとしては、毎年メジャーバージョンアップが行われ 2 年毎に LTS が付くという事なので、今年の.NET 7 は STS で 18 ヶ月間のサポートという事になります。
なかなか実業務には取り込みずらいリリースになりますが、C#言語機能・ライブラリ機能共に多くの機能向上・機能改善が行われています。
ここでは System.Text.Json の機能強化点をピックアップして.NET 7 を探求する 1 つのきっかけになればと思います。

System.Text.Json のシリアル化の変更点

公式ドキュメント「.NET7 の新機能」でも以下の機能強化が謳われています。

  • ユーザー定義型階層のポリモーフィックシリアル化
  • 必須プロパティ
  • JSON コントラクトのカスタマイズ

ここでは上 2 つについて見て行こうと思います。

learn.microsoft.com

ポリモーフィックシリアル化(派生クラスのシリアル化改善)

これまで派生クラスのプロパティのシリアル化には若干の癖(?罠?)の様な振る舞いがありました。
以下のような Pc クラス、それを継承する NotePc クラス / DesktopPc クラス があったとします。

internal class Pc
{
  public string Cpu { get; set; }
  public int Memory { get; set; }
}

// ノートPC
internal class NotePc : Pc
{
  public int MonitorSize { get; set; }
}

// デスクトップPC
internal class DesktopPc : Pc
{
  public int ExtensionSlot { get; set; }
}

これらのクラスのインスタンスを JsonSerializer.Serialize()でシリアライズする実装と実行結果は以下になります。

using System.Text.Json;

// インデントで成形されるようにオプション設定
var options = new JsonSerializerOptions() { WriteIndented = true };

{ // Pcオブジェクトのシリアライズ
  var pc = new Pc() { Cpu = "Intel Core i7-13700K", Memory = 64 };
  var serialisedText = JsonSerializer.Serialize<Pc>(pc, options);
  Console.WriteLine("-- Pc --");
        Console.WriteLine(serialisedText);
}
{ // NotePcオブジェクトのシリアライズ
  var notePc = new NotePc() {
    Cpu = "Intel Core i7-13700K",
    Memory = 64, MonitorSize = 13 };
  var serialisedText = JsonSerializer.Serialize<Pc>(notePc, options);
  Console.WriteLine("-- NotePc --");
  Console.WriteLine(serialisedText);
}
{ // DesktopPcオブジェクトのシリアライズ
  var desktopPc = new DesktopPc() {
    Cpu = "Intel Core i7-13700K",
    Memory = 64, ExtensionSlot =6  };
  var serialisedText = JsonSerializer.Serialize<Pc>(desktopPc, options);
  Console.WriteLine("-- DesktopPc --");
  Console.WriteLine(serialisedText);
}

// 実行結果
// -- Pc --
// {
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// -- NotePc --
// {
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// -- DesktopPc --
// {
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64

派生クラスである NotePc と DesktopPc の固有プロパティは Json データから失われました。
(JsonSerializer に< TValue >として Pc を渡しているので当然と言えば当然ですが)

JsonDerivedTypeAttribute の追加

.NET 7 では JsonDerivedTypeAttribute というものが追加されました。
これを使用すると派生クラスの固有プロパティが Json シリアル化されるようになります。
使い方は簡単で派生元である Pc クラスにサブタイプ(継承クラス)となりうるクラスを明示指定します。

[JsonDerivedType(typeof(NotePc))]
[JsonDerivedType(typeof(DesktopPc))]
[JsonDerivedType(typeof(Pc))]
internal class Pc
{
  public string Cpu { get; set; }
  public int Memory { get; set; }
}

// 実行結果
// -- Pc --
// {
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// -- NotePc --
// {
//   "MonitorSize": 13,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// -- DesktopPc --
// {
//   "ExtensionSlot": 6,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64

デシリアライズは?

シリアライズはうまくいきました。ではその Json データをデシリアライズして C#オブジェクトに変換してみましょう(JsonSerializer.Deserialize())。

{ // Pcオブジェクトのシリアライズ
  var pc = new Pc() { Cpu = "Intel Core i7-13700K", Memory = 64 };
  var serialisedText = JsonSerializer.Serialize<Pc>(pc, options);
  Console.WriteLine("-- Pc --");
  Console.WriteLine(serialisedText);

  // シリアライズしたJsonテキストから再度オブジェクトにデシリアライズ
  var deserializedPc = JsonSerializer.Deserialize<Pc>(serialisedText);
  Console.WriteLine($"デシリアライズされたクラス型 = {deserializedPc.GetType()}");
}
{ // NotePcオブジェクトのシリアライズ
  var notePc = new NotePc() {
    Cpu = "Intel Core i7-13700K",
    Memory = 64, MonitorSize = 13 };
  var serialisedText = JsonSerializer.Serialize<Pc>(notePc, options);
  Console.WriteLine("-- NotePc --");
  Console.WriteLine(serialisedText);

  // シリアライズしたJsonテキストから再度オブジェクトにデシリアライズ
  var deserializedPc = JsonSerializer.Deserialize<Pc>(serialisedText);
  Console.WriteLine($"デシリアライズされたクラス型 = {deserializedPc.GetType()}");
}
{ // DesktopPcオブジェクトのシリアライズ
  var desktopPc = new DesktopPc() {
    Cpu = "Intel Core i7-13700K",
    Memory = 64, ExtensionSlot = 6 };
  var serialisedText = JsonSerializer.Serialize<Pc>(desktopPc, options);
  Console.WriteLine("-- DesktopPc --");
  Console.WriteLine(serialisedText);

  // シリアライズしたJsonテキストから再度オブジェクトにデシリアライズ
  var deserializedPc = JsonSerializer.Deserialize<Pc>(serialisedText);
  Console.WriteLine($"デシリアライズされたクラス型 = {deserializedPc.GetType()}");
}

// 実行結果
// -- Pc --
// {
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = Pc
// -- NotePc --
// {
//   "MonitorSize": 13,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = Pc
// -- DesktopPc --
// {
//   "ExtensionSlot": 6,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = Pc

想定外?想定通り?にすべてが Pc オブジェクト になってしましました。あまりうれしくない結果ですよね。
それぞれ Pc / NotePc / DesktopPc オブジェクトにデシリアライズされて欲しいという想いがあるのではないでしょうか。

では、その部分の改善を行いましょう。
以下のように JsonDerivedType 属性の指定に typeDiscriminator を追加します。

[JsonDerivedType(typeof(NotePc), typeDiscriminator: "notePc")]
[JsonDerivedType(typeof(DesktopPc), typeDiscriminator: "desktopPc")]
[JsonDerivedType(typeof(Pc), typeDiscriminator: "pc")]
internal class Pc
{
  public string Cpu { get; set; }
  public int Memory { get; set; }
}

// 実行結果
// -- Pc --
// {
//   "$type": "pc",
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = Pc
// -- NotePc --
// {
//   "$type": "notePc",
//   "MonitorSize": 13,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = NotePc
// -- DesktopPc --
// {
//   "$type": "desktopPc",
//   "ExtensionSlot": 6,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
//
// デシリアライズされたクラス型 = DesktopPc

期待通りのデシリアライズ結果
期待通りのデシリアライズが行われました。
と同時にシリアライズ時の Json データに「$type」という要素が追加されました。この Json データが表す型を表しています。

$type のカスタマイズ

更に JsonPolymorphicAttribute を追加すると、 $type の名称を自由に変更することが可能です。

[JsonDerivedType(typeof(NotePc), typeDiscriminator: "notePc")]
[JsonDerivedType(typeof(DesktopPc), typeDiscriminator: "desktopPc")]
[JsonDerivedType(typeof(Pc), typeDiscriminator: "pc")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$hogehoge")]
internal class Pc
{
  public string Cpu { get; set; }
  public int Memory { get; set; }
}

// 実行結果
// -- Pc --
// {
//   "$hogehoge": "pc",
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = Pc
// -- NotePc --
// {
//   "$hogehoge": "notePc",
//   "MonitorSize": 13,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
// デシリアライズされたクラス型 = NotePc
// -- DesktopPc --
// {
//   "$hogehoge": "desktopPc",
//   "ExtensionSlot": 6,
//   "Cpu": "Intel Core i7-13700K",
//   "Memory": 64
// }
//
// デシリアライズされたクラス型 = DesktopPc

必須プロパティ

Json データのデシリアライズの際にプロパティの必須化の指定もサポートされました。
必須としたいプロパティに JsonRequired 属性を追加します。
以下のように Cpu / Memory に[JsonRequired]属性を追加し、それらが欠落した Json データをデシリアライズしようとした場合、例外が発生します。

[JsonDerivedType(typeof(NotePc), typeDiscriminator: "notePc")]
[JsonDerivedType(typeof(DesktopPc), typeDiscriminator: "desktopPc")]
[JsonDerivedType(typeof(Pc), typeDiscriminator: "Pc")]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$hogehoge")]
internal class Pc
{
  [JsonRequired]
  public string Cpu { get; set; }
  [JsonRequired]
  public int Memory { get; set; }
}

...

// ↓↓↓ 例外発生 Unhandled exception. System.Text.Json.JsonException: 
//       JSON deserialization for type 'NotePc' was missing required
//       properties, including the following: Cpu, Memory
var deserializedPc = JsonSerializer.Deserialize<Pc>("""
{
  "$hogehoge": "notePc",
  "MonitorSize": 13
}
""");

また、C#言語仕様としてプロパティに required キーワード を指定する方法がありますが、これは Json デシリアライズ処理には適用されないので、JsonRequired 属性を指定する必要があります。

internal class Pc
{
  public required string Cpu { get; set; }
  public required int Memory { get; set; }
}

// ↓これはコンパイル時チェックでビルドエラーとなる
var pc = new Pc(); 

// ↓これは通ってしまう
var deserializedPc = JsonSerializer.Deserialize<Pc>("""
  {
  "$hogehoge": "notePc",
  "MonitorSize": 13
  }
  """);

まとめ

System.Text.Jsonの.NET 7でのちょっとした機能強化点について見てみました。
かなり成熟度が高くなってきた.NETもまだまだ毎年のアップデートで機能強化されています。
.NET 7はSTSではありますが、LTSとなる.NET 8に向けてという意味でも機能強化に追随し、最新の技術で最良のソリューションをユーザに届けられるようにしたいですね。

【最後に】私たちは仲間を募集しています!

ネクストスケープでは一緒に働く仲間を募集しています。
私たちは新しい技術をどんどん取り入れ、そしてお客様のビジョン(事業に対する想い、夢)を実現しています。
最新の技術を駆使したいエンジニア、技術で課題を解決したいエンジニア、アジャイルやスクラムなどの開発手法で強いチームを作りたいPM等々、興味がある方は是非以下からエントリーをお待ちしております。

www.nextscape.net