JSON contractでシリアライズ結果を操作する

C#

.NETでJSONを扱う際に利用する「System.Text.Json」ライブラリには、JSON Contract という仕組みがあります。これは、.NETの型がどのようにシリアライズ・デシリアライズされるかを定義するものです。

このcontractをカスタマイズすることで、以下のような制御が可能になります

  • 特定のプロパティをシリアライズ対象から除外する
  • プライベートプロパティを含める
  • 値を変換・加工してシリアライズする

本記事では、主にシリアライズ処理を変更する例を中心に解説します。

前提

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
.NET8.0.304
IDEVisual Studio 2022 Community

記事のコードは、解説をわかりやすくするために、エラーチェックなど一部の処理を省略しています。

事前準備

以下のような簡単なレスポンスを返却するAPIを用意して、カスタムcontractを指定することでどのように結果が変化するかを確認します。

特定のプロパティを除外

カスタム属性の作成

まず、シリアライズから除外したいプロパティを判定するためのカスタム属性を定義します(この部分はContractの機能とは直接関係ありません)。

public class NotSerializedAttribute : Attribute { }

カスタムcontractの作成

JsonTypeInfo」型の引数を1つ受け取るメソッド(Modifiers)を作成します。この中で特定のプロパティをシリアライズ対象外にする処理を記載します。具体的には、プロパティ情報をループして、属性が付与されているプロパティを見つけ出します。見つかったら、「ShouldSerialize」プロパティにfalseを設定します。

public static class Modifiers
{
	public static void Filter(JsonTypeInfo ti)
	{
		foreach (var pi in ti.Properties)
		{
			object[] attributes = pi.AttributeProvider?.GetCustomAttributes(typeof(NotSerializedAttribute), true) ?? [];
			NotSerializedAttribute? attribute = attributes.Length == 1 ? (NotSerializedAttribute)attributes[0] : null;
			if (attribute is not null)
			{
				pi.ShouldSerialize = (_, _) => false;
			}
		}
	}
}

結果確認

作成したModifiersを使用するためには、JsonSerializerOptionsに指定します。

		JsonSerializerOptions? options = 
			new()
			{
				TypeInfoResolver = new DefaultJsonTypeInfoResolver
				{
					Modifiers = { Modifiers.Filter }
				}
			};
		return new JsonResult(response, options);

プロパティにも属性を付与します。

public class ResponseModel
{
	public int Id { get; set; }
	public string? Password { get; set; }
	[NotSerialized]
	public string? Email { get; set; }

}

属性を付与したEmailプロパティだけが結果から除外されています。

プロパティを追加

既存のモデルに含まれていないプロパティを、シリアライズ時に動的に追加することも可能です。

カスタム属性の定義

CreateJsonPropertyInfoメソッドで新しいプロパティを定義します。そしてgettersetterを追加します。今回はシリアライズ時にプロパティを追加したいだけなのでsetternullを指定しています。

	public static void AddTime(JsonTypeInfo ti)
	{
		if (ti.Kind != JsonTypeInfoKind.Object || ti.Type != typeof(ResponseModel)) return;

		JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "Now");
		property.Get = _ => DateTime.Now.ToString();
		property.Set = null;
		ti.Properties.Add(property);
	}

結果確認

具体的な使い道は思いつきませんが、以下のようにモデルに定義していないプロパティまでレスポンスに含まれています。

値を変更する

プログラム上は元の値を保持したまま、シリアライズ時だけ加工する例です。ここではマスキング処理を行います。

カスタム属性の作成

属性にはマスク化で使用する文字列を指定できるようにしておきます。

public class HideAttribute : Attribute
{
	public string Value { get; init; }
	public HideAttribute(string value)
	{
		Value = value;
	}
}

カスタムcontractの作成

基本的な流れは今までと同様です。値をマスク化して返却するようにgetterをカスタマイズします。既存の値の先頭と末尾の1文字だけを残し、その間をマスク化文字列に置換するサンプルです。

public static class Modifiers
{
	public static void HideValue(JsonTypeInfo ti)
	{
		foreach (var pi in ti.Properties)
		{
			if (pi.PropertyType != typeof(string)) continue;

			object[] attributes = pi.AttributeProvider?.GetCustomAttributes(typeof(HideAttribute), true) ?? [];
			HideAttribute? attribute = attributes.Length == 1 ? (HideAttribute)attributes[0] : null;
			if (attribute is null) continue;

			var getProperty = pi.Get;
			if (getProperty is not null)
			{
				pi.Get = obj =>
				{
					var value = getProperty(obj);
					if (value is null) return null;
					return HideString(value.ToString(), attribute.Value);
				};
			}
		}
	}

	private static string HideString(string? value, string hideValue = "*****")
	{
		string first = string.IsNullOrEmpty(value) ? string.Empty : value[..1];
		string last = string.IsNullOrEmpty(value) ? string.Empty : value[^1..];
		return $"{first}{hideValue}{last}";
	}
}

結果確認

PasswordとEmailを異なる文字列でマスク化します。

public class ResponseModel
{
	public int Id { get; set; }
	[Hide("*****")]
	public string? Password { get; set; }
	[Hide("+++++")]
	public string? Email { get; set; }
}

まとめ

System.Text.JsonJSON contractを活用することで、より柔軟かつ効率的なシリアライズ処理が可能になります。これにより、従来のカスタムコンバーターよりもパフォーマンスと保守性に優れた実装が行えます。

コメント