Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

TJsonSerializerの使い方。

待っててもdocwikiに空ページすら作られる気配が無いので、使い方を調べてみました。

System.JSON.SerializersとSystem.JSON.Convertersユニット

この2つは10.2 Tokyoで新しく追加されたJSON関連のユニットです。2つありますがメインはSystem.JSON.Serializersで、System.JSON.ConvertersはSerializers内で使用するためのコンバータクラス(詳細は後述)が定義されています。

TJsonSerializer

System.JSON.Serializersは名前の通り、JSONシリアライズ/デシリアライズするためのユニットで、その機能はTJsonSerializerクラスに集約されています。関連するカスタム属性を使うことで柔軟かつ複雑な処理を行うこともできますが、必要最低限のコードは以下の通り非常にシンプルです。

uses
  System.JSON.Serializers;

type
  TFoo = record
    Field1: Integer;
    Field2: string;
  end;

procedure Sample;
var
  foo: TFoo;
  serializer: TJsonSerializer;
begin
  foo.Field1 := 123;
  foo.Field2 := 'ABC';
  serializer := TJsonSerializer.Create;
  try
    // シリアライズ
    ShowMessage(serializer.Serialize(foo)); // -> '{"Field1":123,"Field2":"ABC"}'

    // デシリアライズ(新規に生成する場合)
    foo := serializer.Deserialize<TFoo>('{"Field1":456,"Field2":"DEF"}');
    ShowMessageFmt('%d, %s', [foo.Field1, foo.Field2]); // -> '456, DEF'

    // デシリアライズ(既存のものに代入する場合)
    serializer.Populate('{"Field2":"XYZ"}', foo);
    ShowMessageFmt('%d, %s', [foo.Field1, foo.Field2]); // -> '456, XYZ'
  finally
    serializer.Free;
  end;
end;

カスタム属性による制御

基本的な使い方が分かったところで、次はカスタム属性を使ったより便利な使い方を紹介します。シリアライズ/デシリアライズの際に使用可能なカスタム属性はこのように複数あります。

カスタム属性 用途 デフォルト値
JsonConverterAttribute コンバータの指定 なし
JsonIgnoreAttribute 無視するメンバの指定 なし
JsonNameAttribute メンバ名を別名で出力する なし(元の識別子)
JsonInAttribute シリアライズするメンバの指定(TJsonMemberSerializationがInの時のみ有効) なし
JsonObjectHandlingAttribute クラス型メンバをデシリアライザ側で生成するかどうか TJsonObjectHandling.Auto
JsonObjectOwnership デシリアライザでクラス型メンバを生成した場合の元のインスタンスをどうするか TJsonObjectOwnership.Auto
JsonSerializeAttribute シリアライズ/デシリアライズするメンバの選び方 TJsonMemberSerialization.Fields

なお、カスタム属性は例えばFooAttributeという名前の場合、Attribute部分を省いてFooとすることが可能なため、以降では省略して記述します。

JsonSerialize属性

このカスタム属性は型に直接指定するもので、メンバのうちどれをシリアライズ/デシリアイズするかの方法を指定します。指定時は列挙型であるTJsonMemberSerialization型の引数を取り、Fields, Public, Inの3つが指定できます。
例えば以下のようなレコードTFooとクラスTBarを考えます。

type
  [JsonSerialize(TJsonMemberSerialization.Fields)]
  TFoo = record
  private
    FValue1: Integer;
  public
    Value2: Integer;
    property Value3: Integer read FValue1 write FValue1;
  end;
  TBar = class
  private
    FValue1: Integer;
  public
    Value2: Integer;
    property Value3: Integer read FValue1 write FValue1;
  end;

これらの型にTJsonMemberSerialization.Fieldsを指定した場合、あるいは何も指定しなかった場合(=デフォルト)、TJsonSerializerは全てのフィールドを処理対象とします。この場合TFooもTBarも同じでFValue1とFValue2が該当します。フィールドのRTTIは可視性にかかわらず全て生成されるため、privateでもpublicでも処理対象となりますが、$RTTI指令でこれが変更されていた場合、TJsonSerializerの処理対象もそれに追随します。
次にTJsonMemberSerialization.Publicを指定した場合を考えます。この場合可視性がpublicなものを処理対象とするため、TFooもTBarでもValue2とValue3が対象となりそうなものですが、実はレコードのプロパティはRTTIが生成されないため、実際にはTFooはValue2のみ処理対象となります。プロパティを持ったレコードを扱う際は注意が必要です。
最後にTJsonMemberSerialization.Inを指定した場合です。これは前の2つと異なり、指定しただけでは何も起きません。このモードではJsonInというカスタム属性と組み合わせることで、任意のメンバを対象とします。例えば以下のようなクラスがあった場合、JsonInを付けたメンバのみがシリアライズ/デシリアライズの対象となります。この場合FValue1とValue3です。

type
  [JsonSerialize(TJsonMemberSerialization.In)]
  TBar = class
  private
    [JsonIn]
    FValue1: Integer;
    FValue2: string;
  public
    [JsonIn]
    property Value3: Integer read GetValue3 write SetValue3;
  end;
JsonIgnore属性

このカスタム属性はシリアライズ/デシリアライズしないメンバに対して指定します。TJsonMemberSerializationのFieldsとPublicは細かい指定ができないため、この属性と組み合わせることで対象となるメンバを調整します。仕様上JsonInと組み合わせで使うこともできますが、動作としてはJsonIgnoreの方が優先されるため、両方する指定する意味はあまりありません。

JsonName属性

このカスタム属性はJSONのキー名を実際のメンバ名とは別のものにする際に指定します。例えば以下のようなレコードを考えます。

type
  TFoo = record
    [JsonName('Id')]
    Field1: Integer;
    [JsonName('Value')]
    Field2: string;
  end;

このレコードをシリアライズすると、{"Id": 123, "Value": "abc"}のようなJSONが得られます。デシリアライズ時も同様に機能します。

JsonObjectHandling属性

このカスタム属性は引数に列挙型のTJsonObjectHandlingを取り、デシリアライズ時のクラス型メンバの生成方法を制御しますが、Deserializeメソッドは無条件で全て新規に生成するため、この属性はPopulateメソッド専用です。
動作モードは3種類あり、Reuseは新しくインスタンスを生成することなく渡されたものをそのまま使います。ただし対象のメンバがnilだった場合は新しく生成します。Replaceは対象のメンバの中身にかかわらず新しく生成した上で代入します。その際に元々入っていたインスタンスの扱いは後述のJsonObjectOwnershipで指定します。Autoは単にカスタム属性とは別にTJsonSerializerが持つObjectHandling設定を使うというだけです。コンポーネントでいうParentColorやParentFontプロパティのようなもので、親設定に従う、というものです。ただTJsonSerializerの初期値もAutoのため、特に変更のない場合Reuseとして動作します。

JsonObjectOwnership属性

このカスタム属性はDeserializeやPopulateで新しくインスタンスが生成された時、元の値を解放するかどうかを決定します。JsonObjectHandlingにReplaceが指定された時にだけ意味を持つ属性です。
動作モードは3種類あり、Ownedは自身が所有権を持つということで、新しいインスタンスが代入された時に元からあったインスタンスを解放します。NotOwnedは代入されても何もしません。AutoはJsonObjectHandlingと同じくTJsonSerializerのObjectOwnershipプロパティに従います。TJsonSerializerの初期値もAutoのため、その場合は実質的にOwnedとして動作します。
…のはずなんですが、動作しません。何を指定してもAuto扱いになってしまうようです。ドキュメントがないため使い方が間違っている可能性もありますが、ソースを見る限りではTJsonDefaultContractResolver.SetPropertySettingsFromAttributes内でこの属性だけ受け渡しがされていないのが原因のような気がします。ただしAutoの場合上述のようにTJsonSerializerの設定を使いますが、ここでの指定は問題ないためNotOwnedが指定したい場合はこれしか方法が無さそうです。ただし全体に適用されてしまうため注意が必要です。

JsonConverter属性

このカスタム属性は引数にコンバータクラスを取り、複雑な構造を持つ型をシリアライズ/デシリアライズしたり、デフォルトの処理とは違う方法でシリアライズ/デシリアライズする際の動作を指定します。
コンバータクラスはTJsonConverterクラスを継承して作成しますが、ある程度使用頻度が高そうなものはSystem.JSON.Convertersユニット内にあらかじめ実装されています。例えばTJsonEnumNameConverterは列挙型のシリアライズに、TJsonStackConverterはTListJSONの配列にシリアライズできます。コンバータを自作する際はこれらが豊富なサンプルになりそうです。

TJsonSerializerのプロパティ

TJsonSerializerにはオプションが複数あり、これを指定することでシリアライズ/デシリアライズの動作を変更することが可能です。全てをテストしたわけではないため、説明はソースからの推測を含みます。

プロパティ名 説明 取り得る値
DateFormatHandling 日付のフォーマット(シリアライズ時のみ) Iso(デフォルト), Unix, FormatSettings
DateParseHandling TDateTime形式としてパースするか否か(デシリアライズ時のみ) None(デフォルト), DateTime
DateTimeZoneHandling TDateTimeのタイムゾーン設定 Local(デフォルト), Utc
FloatFormatHandling NaNなどの特殊な小数値の出力設定 String(デフォルト), Symbol, DefaultValue
Formatting インデント設定(シリアライズ時のみ) None(デフォルト), Indented
MaxDepth 読み取るネストの深さ(デシリアライズ時のみ) Integer(デフォルト=-1(無制限))
ObjectHandling JsonObjectHandling属性の全体設定 Auto(デフォルト), Reuse, Replace
ObjectOwnership JsonObjectOwnership属性の全体設定 Auto(デフォルト), Owned, NotOwned
StringEscapeHandling 文字列のエスケープ(シリアライズ時のみ) Default(デフォルト), EscapeNonAscii, EscapeHtml

実行時にカスタム属性を変更

カスタム属性は通常設計時に指定するため、ある時はFieldAとFieldBを、ある時はFieldAとFieldCを…というように、実行時に出力したいメンバを変更することができません。このような時はTJsonDynamicContractResolverを使用することで、型やメンバに紐付いたカスタム属性を実行時に動的に変更が可能なようです。
これについてはまだ実際に使用していないので憶測になってしまいますが、TJsonSerializerにContractResolverというIJsonContractResolver型のプロパティがあり、ここにTJsonDynamicContractResolverのインスタンスを代入して使用するようです。動作原理としてはTJsonSerializerがシリアライズする際、カスタム属性をIJsonContractResolver経由で取得するようなのですが、その際にTJsonDynamicContractResolverであらかじめ上書きされたカスタム属性を本来のカスタム属性より優先的に返すことで、実行時にカスタム属性の付け替えを擬似的に行っているようです。