😸

System.Text.Json を試してみる on .NET 6

2021/12/07に公開約6,700字

.NET 5 くらいから?.NET Core 3.1 から?忘れたのですが System.Text.Json が登場して今まで .NET で JSON を扱うんだったら Newtonsof.Json (JSON.NET) といった風潮だったのが変わってきました。
パフォーマンスもいいらしいんですが、痒い所に手が届かないので System.Text.Json 使おうとしたけど最終的に Newtonsoft.Json に置き換えた…ということも何回かありました。一番デカかったのは DateTimeOffset がきちんとサポートされていなかったということでした…。でも .NET 6 では大丈夫。

.NET 6 になって色々改善されたり、ソースジェネレーターでクラスのメタデータを定義するコードやシリアライズのコードを生成してくれるようになるのでリフレクションベースのものより早い!!という風になってるらしいのでためしてみようと思います。

ドキュメントしては以下の部分ですね。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-overview?pivots=dotnet-6-0

注意点としては System.Text.Json は従来通りリフレクションを使ってシリアライズ・デシリアライズに必要なメタデータを集めて、シリアライズ・デシリアライズを行うモードがあります。機能的にはこのモードが一番サポートしている機能が多いです。使えない機能はきちんと把握したうえで使う必要があります。以下のドキュメントにサポートされていない機能の表があるので、そこには目を通しておきましょう。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-source-generation-modes?pivots=dotnet-6-0

とりあえず使う

System.Text.Json をとりあえず使うだけなら凄く簡単です。コンソールアプリで以下のようなコードを書くだけで使えます。

using System.Text.Json;

var emp = new Employee("Tanaka");
var json = JsonSerializer.Serialize(emp);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json);
Console.WriteLine(emp2);

public record Employee(string Name);

実行すると以下のような結果になります。

{"Name":"Tanaka"}
Employee { Name = Tanaka }

いい感じですね。これは従来通りのリフレクションが使われています。

ソース生成してもらう

ソースジェネレーターを使うには以下の手順を踏む必要があります。

JsonSerializerContext を継承した partial なクラスを定義します。このクラスに JsonSerializableAttribute を指定してシリアライズ、デシリアライズしたい型を指定します。

ということで、この通りにクラスを宣言してみましょう。先ほどの Employee レコードをシリアライズ対象にしています。

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

この定義を追加すると System.Text.Json のクラスがソースコードの生成をしてくれます。

この状態で SerializeDeserialize メソッドに MyJsonSourceGenerationContext.Default.クラス名 を渡してやればソースジェネレーターで生成されたコードが使われるようになります。

using System.Text.Json;
using System.Text.Json.Serialization;

var emp = new Employee("Tanaka");
var json = JsonSerializer.Serialize(emp, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(emp2);

public record Employee(string Name);

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

実行結果は変わらないので割愛します。

シリアル化のみ(Deserializeはしない)ような場合には以下のように JsonSourceGenerationMode.Serialization を設定します。

[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

こうするとシリアライズのみに絞ることが出来ます。

注意点ta

ソースジェネレーターでは init プロパティが現状ではサポートされていません。なので以下のようなコードを書くと…

using System.Text.Json;
using System.Text.Json.Serialization;

var emp = new Employee { Name = "Tanaka" };
var json = JsonSerializer.Serialize(emp, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(emp2);

public class Employee
{
    public string? Name { get; init; } // init !!
}

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

実行時に例外になります。

レコードのようにコンストラクタがあればプロパティが init だろうと private だろうと OK です。

using System.Text.Json;
using System.Text.Json.Serialization;

var emp = new Employee("Tanaka");
var json = JsonSerializer.Serialize(emp, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(emp2);

public record Employee(string Name);

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

つまり以下のようなクラスも OK

using System.Text.Json;
using System.Text.Json.Serialization;

var emp = new Employee("Tanaka");
var json = JsonSerializer.Serialize(emp, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(emp2);

public class Employee
{
    public Employee(string name)
    {
        Name = name;
    }

    public string Name { get; }

    public override string ToString()
    {
        return $"{nameof(Employee)}: {nameof(Name)} = {Name}";
    }
}

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

コンストラクターのパラメーターの名前とプロパティの名前は一致している必要があるみたいです。
例えば Employee クラスのコンストラクタのパラメータ名を hogeeeeeeeee のようにすると…

using System.Text.Json;
using System.Text.Json.Serialization;

var emp = new Employee("Tanaka");
var json = JsonSerializer.Serialize(emp, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(json);

var emp2 = JsonSerializer.Deserialize<Employee>(json, 
    MyJsonSourceGenerationContext.Default.Employee);
Console.WriteLine(emp2);

public class Employee
{
    public Employee(string hogeeeeeeeeee)
    {
        Name = hogeeeeeeeeee;
    }

    public string Name { get; }

    public override string ToString()
    {
        return $"{nameof(Employee)}: {nameof(Name)} = {Name}";
    }
}

[JsonSerializable(typeof(Employee))]
internal partial class MyJsonSourceGenerationContext : JsonSerializerContext { }

以下のような例外が出ます。

複数のコンストラクタがあるケースで、デシリアライズに使うコンストラクタを指定したい場合は JsonConstructor 属性をコンストラクタにつけることで指定可能です。以下のドキュメントに記載があります。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-6-0

実際使う上でお世話になりそうなドキュメントのページ

プロパティ名をキャメルケースにしたいというのは C#er が JSON を扱うときに必ず通る道…。JsonSerializerOptionsPropertynamingPolicyJsonNamingPolicy.CamelCase を指定すれば OK です。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-customize-properties?pivots=dotnet-6-0

循環参照の扱いは多分お世話になるので呼んでおく必要がありそうです。ただ、ソースジェネレーターでは ReferenceHandler がサポートされてなさそう…

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-preserve-references?pivots=dotnet-6-0

最後の手段としてはカスタムコンバーターを書く必要があるケースも出てくるので、ここらへんも大事そう。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0

まとめ

個人的には循環参照さえなければ…ソースジェネレーター検討できそうな気がする…。
でも稀によくあるしなぁ…。

Discussion

ログインするとコメントできます