System.Text.Json で static プロパティをシリアライズする方法(JSON Contract 使用)

C#

前回の記事では、JSON Contract を使用してシリアライズ処理をカスタマイズする方法をご紹介しました。
.NET の System.Text.Json ライブラリでは、static なプロパティはデフォルトでシリアライズ対象になりません。

今回は JSON Contract を活用して、static プロパティをシリアライズ結果に含める方法を解説します。

前提

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

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

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

テスト用データ

まずは、シリアライズの挙動を確認するためのモデルを用意します。

public class StaticTest
{
	public int NonStatic { get; set; }
	public static int Static1 { get; set; }
	public static int Static2 { get; set; }
}

次に、簡単なAPIを用意します。

	[HttpGet]
	[Route("/static/test")]
	public async Task<IActionResult> TestStatic()
	{
		var s = new StaticTest
		{
			NonStatic = 1,
		};
		StaticTest.Static1 = 1;
		StaticTest.Static2 = 1;
		return Ok(s);
	}

実行結果を確認すると、static プロパティはシリアライズ結果に含まれていません。

Contract作成

カスタム属性の定義

まずは、シリアライズ対象とするプロパティを判定するためのカスタム属性を作成します。
これは必須ではありませんが、対象プロパティを簡単に特定できるようにしておくと便利です。

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class JsonIncludeStaticAttribute : Attribute { }

Contract作成

JsonTypeInfo を引数に取る Modifier メソッドを作成します。

typeInfo には StaticTest モデル(シリアライズ対象)の情報が渡されます。
最初の 2 つの if 文で、対象外の型であれば処理を終了します。
次に、BindingFlags.Static | BindingFlags.Public で static プロパティを取得し、カスタム属性が付与されていればシリアライズ対象に追加します。

重要なポイントは getter の設定です。
シリアライズ時には getter が呼び出されるため、JsonPropertyInfo.Get に static プロパティの getter を設定する必要があります。

	public static void IncludeStaticModifier(JsonTypeInfo typeInfo)
	{
		if (typeInfo.Kind != JsonTypeInfoKind.Object) return;
		if (!typeInfo.Type.IsDefined(typeof(JsonIncludeStaticAttribute), false)) return;
		var propertyInfoList = typeInfo.Type.GetProperties(BindingFlags.Static | BindingFlags.Public);

		foreach (var propertyInfo in propertyInfoList)
		{
			if (HasAttribute<JsonIncludeStaticAttribute>(propertyInfo))
			{
				JsonPropertyInfo jsonPropertyInfo = typeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType, propertyInfo.Name);
				jsonPropertyInfo.Get = CreateGetter(propertyInfo);
				typeInfo.Properties.Add(jsonPropertyInfo);
			}
		}
	}

	private static bool HasAttribute<T>(PropertyInfo propertyInfo)
	{
		object[]? customAttributes = propertyInfo.GetCustomAttributes(typeof(T), true);
		if (customAttributes is not null && customAttributes.Length == 1)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

それでは具体的に getter の作成処理を見ていきます。こちらの処理は Stack Overflowの回答を流用いたしました(一部変更しています)ので、詳細はそちらをご確認ください。

GetProperties で取得できる PropertyInfo から GetGetMethod() を呼び出すと getter MethodInfo を取得できます。しかし JsonPropertyInfo.Get に設定すべき型は Func<object, object?>? です。そのため、MethodInfo 型を Func<object, object?>? 型に変換します。

そこで、CreateDelegate メソッドを使用してデリゲートを作成し、実行時に型を動的に指定します(MakeGenericMethod)。ジェネリックメソッドの型引数Tは、実行時にならないと決まりません(staticプロパティの型に依存するため)。そのため、CreateDelegate を実行する処理を別メソッドに切り出して、リフレクションを使用することで動的に型を指定できるようにしています。

	static Func<object, object?>? CreateGetter(PropertyInfo propertyInfo)
	{
		if (!(propertyInfo.GetGetMethod() is { } getMethod)) return null;
        // publicでないstaticメソッドを検索対象とする
		var typedCreator = typeof(CustomJsonContracts).GetMethod(nameof(CreateGetterFunc), BindingFlags.Static | BindingFlags.NonPublic);
		// MakeGenericMethodの引数にジェネリック型に渡す型を指定する
        var getterFunc = typedCreator!.MakeGenericMethod(propertyInfo.PropertyType);
        // staticメソッドの場合、第一引数はnull
		return (Func<object, object?>?)getterFunc.Invoke(null, [getMethod]);
	}

	static Func<object, object?> CreateGetterFunc<T>(MethodInfo methodInfo)
	{
		var typedFunc = methodInfo.CreateDelegate<Func<T?>>();
		return (o => typedFunc());
	}

動作確認

まずはモデルに属性を付与します。

[JsonIncludeStatic]
public class StaticTest
{
	public int NonStatic { get; set; }
	[JsonIncludeStatic]
	public static int Static1 { get; set; }
	public static int Static2 { get; set; }
}

次に、シリアライズ時に JSON Contract を指定します。

	[HttpGet]
	[Route("/static/test")]
	public async Task<IActionResult> TestStatic()
	{
		var s = new StaticTest
		{
			NonStatic = 1,
		};
		StaticTest.Static1 = 1;
		StaticTest.Static2 = 1;
		JsonSerializerOptions serializerOption = new()
		{
			WriteIndented = true,
			TypeInfoResolver = new DefaultJsonTypeInfoResolver
			{
				Modifiers = { CustomJsonContracts.IncludeStaticModifier }
			}
		};
		var json = JsonSerializer.Serialize(s, serializerOption);
		return Ok(json);
	}

実行すると、属性を付与した static プロパティも結果に含まれるようになります。

まとめ

  • System.Text.Json では、デフォルトでは static プロパティはシリアライズされない
  • JSON Contract を使用すると、static プロパティも対象に含められる
  • getter の設定には CreateDelegate とリフレクションが必要
  • カスタム属性を使うと対象プロパティを明示できる

コメント