アプリ開発ときどきアウトドア

主にJavaを使ったアプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

.NET Core 1. システムエンジニアリング 実装技術

.NET Core: JsonSerializerの実践的な使い方

投稿日:

業務アプリの開発を想定したJsonSerializerの使い方とサンプルです。

概要

業務想定のサンプル

  • 日本語(UTF8)の値の読み取り・書き込み
    日本語を含むUTF8形式の値の読み書きを可能とするためにUtf8JsonReader/Utf8JsonWriterを使用します。
    これらは性能向上のためにref struct型として宣言されており、そのままではasyncメソッド(非同期メソッド)で使用できません。単純な解決策として、同期メソッドで使用しています。
  • 既定の日付書式の変更
    DateTime型の既定の日付書式は、”2020/11/22T03:23:45″, “2020/11/22T12:23:45+09:00″等のISO形式です。この書式は業務によって変わってくるので、カスタマイズできるようにします。詳細は後述します。
  • JSONフィールド名にキャメルケースを使用
    既定では、JSONに出力されるフィールド名はパスカルケースになります。例えば、MyNameプロパティは”MyName”というフィールド名で出力されます。経験的にキャメルケースのJSONを扱うことが多いので、ここではキャメルケース(“myName”)で出力されるように変更します。
  • コメントのスキップ
    既定ではJSONファイル上の”//”, “/* … */”のコメントはエラーになってしまいます。
    外部システムとのデータ交換では、処理効率が優先されるため、冗長なデータとなるコメントは使用されません。設定ファイルの定義や動作確認・テストのような場面では保守性や開発効率を向上できるため、コメントの使用を許容します。
public class Example3
{
    public static void Run()
    {
        var json = LoadJson("input.json");
        WriteJson("output.json", json);
    }

    public static BasicExampleElement LoadJson(string filename)
    {
        // 日本語を含むUTF8(BOMなし)のファイルを前提
        var bytes = File.ReadAllBytes(filename);

        // 日本語を扱うためにUtf8JsonReaderを使用
        var readerOptions = new JsonReaderOptions()
        {
            CommentHandling = JsonCommentHandling.Skip, // コメントをスキップ
        };
        var reader = new Utf8JsonReader(bytes, readerOptions);

        // デシリアライズ時のオプション
        var serializerOptions = new JsonSerializerOptions()
        {
            // キャメルケースとプロパティ名の相違に対応
            // ・例: JsonのmyNameフィールドをMyNameプロパティに対応付け
            // ・大小文字を区別する場合は[JsonPropertyName]を使用のこと
            PropertyNameCaseInsensitive = true,
        };

        // DateTimeの既定の日付書式を変更
        // (既定のISO形式ではなく独自の日付書式に変更)
        serializerOptions.Converters.Add(new DateTimeLocalConverter());

        // JSONの読み取り
        return JsonSerializer.Deserialize<BasicExampleElement>(ref reader, serializerOptions);
    }

    public static void WriteJson(string filename, BasicExampleElement element)
    {
        using var stream = new FileStream(filename, FileMode.OpenOrCreate | FileMode.CreateNew);

        // 日本語を扱うためにUtf8JsonReaderを使用
        var writerOptions = new JsonWriterOptions()
        {
            // 日本語文字のユニコードエスケープを防止
            // (例えば "あ" が "\u3042" のようにエスケープされないようにする。)
            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
            // 人が参照しやすいようインデントを付与(不要であれば、このオプションは除外)
            Indented = true
        };
        var writer = new Utf8JsonWriter(stream, writerOptions);

        // シリアライズ時のオプション
        var serializerOptions = new JsonSerializerOptions()
        {
            // キャメルケースでJsonフィールドを使用(既定はパスカルケース)
            // (例: MyNameプロパティは、myNameというJsonフィールドとして出力)
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        // DateTime既定の日付書式を変更
        serializerOptions.Converters.Add(new DateTimeLocalConverter());

        // JSONの出力
        JsonSerializer.Serialize(writer, element, serializerOptions);
    }

}
  • Utf8JsonReader/Utf8JsonWriterで指定可能なオプションは次をご覧ください。
  • シリアライザで指定可能なオプションは次をご覧ください。
  • UTF8のBOM有り形式のファイルの場合、JSON解析時に次のエラーが発生します。任意のテキストエディタで開き、UTF8(BOMなし)で保存し直してください。
    ‘0xEF’ is an invalid start of a value. Path: $ | LineNumber: xx | BytePositionInLine: xx.

変換のカスタマイズ

  • クラス・プロパティとJSONフィールドの変換はコンバータによって実現されています。
  • 標準で多様なコンバータが提供されています。
    • 一般的な型とJSONとの変換例をこちらに纏めてみました。
    • 標準のコンバータのソースコードはこちらで公開されています。
  • 標準のコンバータでは実現できない変換については、独自のコンバータを定義することもできます。
    以降では、典型的なコンバータのサンプルを説明します。
  • コンバータを使う方法以外にも、ファクトリーパターン(JsonConverterFactory)を使用する方法もありますが、こちらは別途説明予定です。

コンバータの使用方法

変換仕様を実装するためのコンバータの定義方法、定義したコンバータを特定の型やプロパティに指定する方法を説明します。

コンバータの定義方法

JsonConverterから派生したクラスを定義することでコンバータを作成できます。
例えば、bool型のtrue/falseをJSONに”yes”/”no”で出力する場合は、次のようにコンバータを定義します。

public class BoolSampleConverter : JsonConverter<bool>
{
    public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.GetString().Equals("yes"); // "yes"ならtrue、それ以外はfalse
    public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        => writer.WriteStringValue(value ? "yes" : "no");
}

4行目のようにReadメソッドで「JSONから取得した文字列をどのように型に変換するか」を定義します。
逆に、6行目のようにWriteメソッドで「型の値をどのようにJSONに出力するか」を定義します。

コンバータの指定方法

定義したコンバータの指定方法は次の3つの方法があります。

  1. 特定の型に対する変換を変更したい場合(既定のコンバータの変更)
    JsonSerializerOptions.Convertersにコンバータを追加します。
        var serializerOptions = new JsonSerializerOptions();
        serializerOptions.Converters.Add(new BoolSampleConverter());
        JsonSerializer.Serialize(writer, element, serializerOptions);
    
  2. 特定プロパティの値を変換したい場合
    プロパティに[JsonConverter]を指定します。
    class BoolExampleElement
    {
    ...
        [JsonConverter(typeof(BoolYesNoConverter))]
        public bool BoolYesNoValue { get; set; }
    ...
    }
    
  3. 特定クラスを変換したい場合(M:N変換、複雑な構造の変換時)
    クラス定義に[JsonConverter]を指定します。
    public class ClassExampleElement
    {
        public string SiteName { get; set; }
        public UriElement Uri { get; set; }
    }
    
    [JsonConverter(typeof(UrlElementConverter))]
    public class UriElement
    {
        public string Scheme { get; set; }
        public string Hostname { get; set; }
        public int PortNumber { get; set; }
    }
    

bool型コンバータの例

bool型のtrue/falseを”yes”/”no”、”on”/”off”等の文字列に変換するコンバータの例を次に示します。
値の読み取り・書き込み等の共通の処理を基底クラスとして実装し、”yes”/”no”等の変換に使用する具体的な値を派生クラスで定義しています。

public class BoolStringConverterBase : JsonConverter<bool>
{
    public string TrueValue { get; }
    public string FalseValue { get; }
    public bool IgnoreCase { get; }
    public BoolStringConverterBase(string trueValue, string falseValue, bool ignoreCase = true)
    {
        TrueValue = trueValue;
        FalseValue = falseValue;
        IgnoreCase = ignoreCase;
    }

    public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.GetString().Equals(TrueValue, IgnoreCase ?
            StringComparison.CurrentCultureIgnoreCase :
            StringComparison.CurrentCulture);

    public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        => writer.WriteStringValue(value ? TrueValue : FalseValue);
}

public class BoolYesNoConverter : BoolStringConverterBase
{
    public BoolYesNoConverter() : base("yes", "no") { }
}

public class BoolOnOffConverter : BoolStringConverterBase
{
    public BoolOnOffConverter() : base("on", "off") { }
}

bool型のtrue/falseを1/0等の整数に変換するコンバータの例を次に示します。

public class BoolIntConverterBase : JsonConverter<bool>
{
    public int TrueValue { get; }
    public int FalseValue { get; }
    public BoolIntConverterBase(int trueValue, int falseValue)
    {
        TrueValue = trueValue;
        FalseValue = falseValue;
    }
    public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.GetInt32().Equals(TrueValue);
    public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value ? TrueValue : FalseValue);
}

public class BoolOneZeroConverter : BoolIntConverterBase
{
    public BoolOneZeroConverter() : base(1, 0) { }
}

DateTime型コンバータの例

DateTime型は既定で、”2020/11/22T03:23:45″, “2020/11/22T12:23:45+09:00″等のISO形式の日付書式になります。
私の経験的にISO形式は使用することは少なく、”2020/11/22 12:23:45″, “20201122”, “2020-06″等の業務独自の日付書式を使うことが多いため、このような日付書式を扱うコンバータの例を次に示します。

前述のコンバータ同様、値の読み取り・書き込み等の共通の処理を基底クラスとして実装し、日付書式を派生クラスで定義しています。

public class DateTimeConverterBase : JsonConverter<DateTime>
{
    public string DateTimeFormat { get; }
    public DateTimeConverterBase(string dateTimeFormat)
    {
        DateTimeFormat = dateTimeFormat;
    }
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => DateTime.ParseExact(reader.GetString(), DateTimeFormat, CultureInfo.CurrentCulture);

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString(DateTimeFormat));
}

public class DateTimeLocalConverter : DateTimeConverterBase
{
    public DateTimeLocalConverter() : base("yyyy/MM/dd HH:mm:ss") { }
}

public class DateTimeYmdConverter : DateTimeConverterBase
{
    public DateTimeYmdConverter() : base("yyyyMMdd") { }
}

public class DateTimeYmHyphenConverter : DateTimeConverterBase
{
    public DateTimeYmHyphenConverter() : base("yyyy-MM") { }
}

列挙型コンバータの例

列挙型の既定の出力)

既定では、列挙型は整数に変換されます。
例えば、次のStatus列挙型のプロパティの値がStartの場合、JSONファイルには1が出力さます。

enum Status
{
    Start = 1, End = 2
}

列挙型の値名の出力

列挙型の整数ではなく、”Start”, “End”等の列挙型の値名で出力する場合は、既定のJsonStringEnumConverterを使用できます。
JsonStringEnumConverterのコンストラクタの引数指定でキャメルケース形式で値を出力することもできますが、これはJsonSerializerOptions.Convertersに追加する方法で指定する必要があります。(または独自のコンバータを作成するか。)

class EnumExampleElement
{
...
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public Status StatusStringValue { get; set; }
...
}

列挙型をカスタム値で出力

列挙型を独自の値に変換するコンバータの例を次に示します。
この例では、前述のStatus列挙型のStart/Endを、”begin”/”finish”に変換します。

public class StatusEnumStringConverter : JsonConverter<Status>
{
    public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        switch (reader.GetString())
        {
            case "begin": return Status.Start;
            case "finish": return Status.End;
            default: return default;
        }
    }
    public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOptions options)
    {
        string result;
        switch (value)
        {
            case Status.Start: result = "begin"; break;
            case Status.End: result = "finish"; break;
            default: result = string.Empty; break;
        }
        writer.WriteStringValue(result);
    }
}

クラスコンバータの例

URLのスキーム/ホスト名/ポート番号をプロパティとして持つクラスを、JSONのURIに変換するコンバータの例を次に示します。
(あくまでもサンプルであり、単純にURI型のプロパティを指定すれば、ほぼ同様のことができると思います。)

public class UrlElementConverter : JsonConverter<UriElement>
{
    public override UriElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var uri = new Uri(reader.GetString());
        return new UriElement()
        {
            Scheme = uri.Scheme,
            Hostname = uri.Host,
            PortNumber = uri.Port
        };
    }

    public override void Write(Utf8JsonWriter writer, UriElement value, JsonSerializerOptions options)
    {
        var uri = new Uri($"{value.Scheme}://{value.Hostname}:{value.PortNumber}");
        writer.WriteStringValue(uri.AbsoluteUri);
    }
}


(adsbygoogle = window.adsbygoogle || []).push({});


(adsbygoogle = window.adsbygoogle || []).push({});

-.NET Core, 1. システムエンジニアリング, 実装技術

執筆者:

関連記事

Javaにおけるファイルパスの正規化

サーバ側でのzipファイルの解凍等の際に、意図しないディレクトリやファイル(ディレクトリトラバーサル攻撃)へのアクセスを防ぐための検証として、絶対パスを正規化したい場合がある。 Fileクラスを使った …

ASP.NET Core: ファイルアップロードの考察

ASP.Net Core(3.1)を使ったファイルアップロードに関する考察です。 元ネタはマイクロソフトのサイトですが、記載内容が私には難しかったり、業務で使用するために悩む部分があったので独自に纏め …

Javaでのパスワード付きzipファイルの圧縮/解凍方法(ZipCrypto/AES)

先日、JavaでのZIP暗号化の考察という記事を書きましたが、zip4jのメンテナンスが再開されており、バージョン2系が公開されていましたので、これを使って通常のzip圧縮/解凍、パスワード付きzip …

mybatis-generatorプラグインの実装方法

mybatis-generatorを使うことで、各テーブルを操作するためのクラス群を容易に準備することができます。しかしながら、mybatis-generatorが提供する機能では、システム開発で求め …

wildflyへのwarデプロイの自動化

更新したWebアプリをWildflyにデプロイするのが面倒なのでスクリプトを作成してみました。 前提 実行環境はCentOS Linux 7です。 JavaEEのWebアプリの配布形式であるwarファ …