sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# System.Text.Json で非数値 (NaN, Infinity) を書込/読込する方法

C#JSON を扱うときは、単純に「System.Text.Json」ケースが増えてきました。DataContractJsonSerializerNewtonsoft.Json のように他の選択肢もあるけど、徐々に移行されていくかと。

System.Text.Json」を利用するケースで、問題になることがあった例のひとつに「非数値 (NaN) の扱い」というものがありました。標準では、非数値を含むクラスなんかのシリアライズは、エラーになってしまう(JSON のフォーマットの問題)ので、独自に JsonConverter<T>設計する必要がありました

「.NET number values such as positive and negative infinity cannot be written as valid JSON」というエラーになります。

こうしたケースだと、 Newtonsoft.Json のほうが、少しだけコンバーターを楽に設計できたりしたのですが、「System.Text.Json」のバージョンが5になって非数値に対応できるようになっていました

待望の機能だったので、とりあえずやってみます。

ただし、まだ System.Text.Json 5 は RC (Release Candidate: リリース候補) の状態です。安定版ではないので注意。

f:id:shikaku_sh:20201016111424p:plain
使用したバージョン

書き込みのテストコード(エラー発生)

こんな感じで、NaN を含めるとエラーになります。ここまでは、今まで通り。

public class ClassWithInts
{
    public int NumberOne { get; set; }
    public double NumberTwo { get; set; }
}

public void Run()
{
    var data = new ClassWithInts
    {
        NumberOne = -1,
        NumberTwo = double.NaN,
    };

    var json = JsonSerializer.Serialize(data);
}

f:id:shikaku_sh:20201016111630p:plain

書き込みのテストコード

System.Text.Json は 5.0 になって、JsonSerializerOptionsNumberHandling プロパティが追加されています。シリアライズするときの処置を補足できます。

以下のようにしてみます。

public void Run()
{
    var options = new JsonSerializerOptions
    {
        NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
        WriteIndented = true,
    };

    var data = new ClassWithInts
    {
        NumberOne = -1,
        NumberTwo = double.NaN,
        NumberPositive = double.PositiveInfinity,
        NumberNegative = double.NegativeInfinity,
    };

    var json = JsonSerializer.Serialize(data, options);

    Console.WriteLine(json);
}

無事、出力できるようになりました。

f:id:shikaku_sh:20201016111709p:plain

読み込みのテストコード(エラー発生)

書き込みと同じで、デフォルトの状態だとエラーが発生します。ここまでは、今まで通り。

public void ReadTest(string json)
{
    // var json = @"{""NumberOne"":-1,""NumberTwo"":""NaN"",""NumberPositive"":""Infinity"",""NumberNegative"":""-Infinity""}";
    var data = JsonSerializer.Deserialize<ClassWithInts>(json, options);
}

f:id:shikaku_sh:20201016111802p:plain:w600

読み込みのテストコード

書き込みと一緒で、JsonSerializerOptionsNumberHandling プロパティを設定して、シリアライズするときの処置を補足します。

public void ReadTest(string json)
{
    var options = new JsonSerializerOptions
    {
        NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
    };

    var data = JsonSerializer.Deserialize<ClassWithInts>(json, options);
}

読み込みできるようになりました。コンバーターいらずなので、すごく楽。

f:id:shikaku_sh:20201016111840p:plain:w600

シリアライズのオプションの意味

追加された JsonNumberHandling のコメントはこんな感じになっています。

  • JsonNumberHandling.AllowReadingFromString
    Numbers can be read from System.Text.Json.JsonTokenType.String tokens Does not prevent numbers from being read from System.Text.Json.JsonTokenType.Number token.

  • JsonNumberHandling.AllowNamedFloatingPointLiterals
    The "NaN", "Infinity", and "-Infinity" System.Text.Json.JsonTokenType.String tokens can be read as floating-point constants, and the System.Single and System.Double values for these constants will be written as their corresponding JSON string representations.

ざっくり訳:

  • JsonNumberHandling.AllowReadingFromString
    System.Text.Json.JsonTokenType.String トークンから、数値を読み取るようにします。ただし、System.Text.Json.JsonTokenType.Number からの数値の読み取りはできません。

  • JsonNumberHandling.AllowNamedFloatingPointLiterals
    System.Text.Json.JsonTokenType.String トークンから "NaN"、"Infinity"、"-Infinity" の値を浮動小数の定数として読み込むことができるようになります。System.SingleSystem.Double の定数は、対応する JSON 文字列表現として書き込みされます。

読み取りのときは JsonNumberHandling.AllowReadingFromString を設定しなくても成功しましたが、提案時点ではつける方針だったみたいなので設定しています。

サンプル

GitHub の「Sample」の中に「TextJsonNaN」のソリューションを追加しました。

参照

Visual C# 2019パーフェクトマスター

Visual C# 2019パーフェクトマスター

Effective C# 6.0/7.0

Effective C# 6.0/7.0