ドメインモデルの JSON シリアライズ用 DTO を自動生成する Source Generator です。
ReactiveProperty<T> や ObservableList<T> をはじめ、[assembly: RegisterJsonConfigWrapper] 属性により任意のラッパー型をプラグインとして登録できます。
通常のシリアライザ(System.Text.Json 等)でドメインモデルを保存・復元しようとすると、以下のような課題に直面します:
- ラッパー型のシリアライズ:
ReactiveProperty<T>などのラッパー型はそのままでは内部値ではなくオブジェクトの内部構造がシリアライズされてしまいます。 - 循環参照のハンドリング: 親子関係や相互参照を持つモデルをシリアライズすると、無限再帰が発生して実行時にクラッシュします。
- DI(依存性注入)との連携: コンストラクタインジェクションを利用しているモデルをデシリアライズで復元する際、標準のシリアライザだけではインスタンス化が困難です。
GenJsonConfig は、Source Generator が最適な DTO を自動生成することで、これらの問題を解決します。
- Source Generator による生成: 実行時のリフレクションを最小化。Native AOT (
JsonSourceGenerationContext) にも対応した高速な動作。 - 型マッピング:
ReactiveProperty<T>→T?、ObservableList<T>→T[]?のように JSON で扱いやすい型へ自動的にマッピング。[assembly: RegisterJsonConfigWrapper]による任意のラッパー型の登録にも対応。 - 循環参照のサポート: 内部的な
___Idと___Refトークンを用いて、オブジェクトの参照関係を保持したまま保存・復元が可能。 - DI コンテナ連携: 生成される
CreateModelメソッドがIServiceProviderを受け取り、DI 経由でモデルを生成・注入。 - ポリモーフィズム対応: インターフェースや基底クラスをプロパティに持つ場合も、属性指定により派生型の識別と保存が可能。
.csproj にジェネレータを Analyzer として追加します。
<ItemGroup>
<ProjectReference Include="..\GenJsonConfig.Generators\GenJsonConfig.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>保存したいモデルに [GenerateJsonConfigDto] 属性を付与します。
using R3;
using ObservableCollections;
using GenJsonConfig.Attributes;
[GenerateJsonConfigDto]
public class MySettings {
// ReactiveProperty は自動的に内部の型にマッピングされます
public ReactiveProperty<string> UserName { get; } = new("Antigravity");
// ObservableList は配列にマッピングされます
public ObservableList<int> Scores { get; } = new();
// 通常のプロパティ(public setter が必要)
public bool IsEnabled { get; set; }
}JsonSerializerContext を定義して、生成された DTO をシリアライズ対象に含めます。
using System.Text.Json.Serialization;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(MySettingsForJson))]
public partial class MyJsonContext : JsonSerializerContext { }自動生成された MySettingsForJson と、定義した MyJsonContext を使用して変換を行います。
// 1. モデルから DTO へ変換して保存
var model = new MySettings();
var dto = MySettingsForJson.CreateJson(model);
// ※ポリモーフィズムを使用する場合は JsonSerializerOptions の設定が必要(後述)
string json = JsonSerializer.Serialize(dto, MyJsonContext.Default.MySettingsForJson);
File.WriteAllText("settings.json", json);
// 2. JSON から DTO を経由してモデルを復元
string loadedJson = File.ReadAllText("settings.json");
var loadedDto = JsonSerializer.Deserialize(loadedJson, MyJsonContext.Default.MySettingsForJson);
// CreateModel は IServiceProvider を通じてモデルを生成します
MySettings restoredModel = MySettingsForJson.CreateModel(loadedDto, serviceProvider)!;ジェネレータは [GenerateJsonConfigDto] が付与されたモデルクラスを解析し、以下の基準に基づいた DTO(末尾 ForJson)を生成します。
モデル内の public プロパティ を走査し、以下の条件に合致するものを DTO の対象として収集します。
| プロパティの種類 | 収集条件 | 特記事項 |
|---|---|---|
| ReactiveProperty(組み込み) | 常に収集 | getter のみ(Setter なし)でも対応可能 |
| ObservableList(組み込み) | 常に収集 | getter のみ(Setter なし)でも対応可能 |
カスタムラッパー型([RegisterJsonConfigWrapper] で登録) |
常に収集 | getter のみでも対応可能。アダプター経由でアクセスコードを生成 |
| 通常のプロパティ | public setter が必須 | デシリアライズ時に値を代入するため |
- 対象外となるプロパティ:
private,internal,protectedなどの public ではないプロパティ。{ get; }のみ、または{ get; private set; }の通常のプロパティ。[ExcludeProperty]属性が付与されているプロパティ。
収集されたプロパティは、その内部型に応じて以下のルールで DTO 側の型にマッピングされます。すべて JSON での欠損(null)を許容するため、DTO 側の型は一律で nullable になります。
| モデル側の型 | DTO 側の型 | 分類 |
|---|---|---|
ReactiveProperty<T> |
T? |
ラッパー型: .Value で getter/setter を生成 |
ReactiveProperty<TModel> |
TModelForJson? |
再帰型: DTO 変換が連鎖 |
ObservableList<T> |
T[]? |
コレクション型: 要素が通常型ならそのまま配列化 |
ObservableList<TModel> |
TModelForJson[]? |
コレクション×再帰型: 各要素を DTO 変換して配列化 |
MyWrapper<T>(カスタム登録) |
T? |
カスタムラッパー型: アダプター経由で getter/setter を生成 |
T(通常プロパティ) |
T? |
そのままのマッピング |
プロパティの型 T 自体に [GenerateJsonConfigDto] が付与されている場合、ジェネレータはその型を DTO 型(TForJson)へ置き替えます。
これにより、モデルのツリー構造全体が連鎖的に DTO 化され、CreateJson / CreateModel 内部でも再帰的に変換メソッドが呼び出されます。
Note
Color のように [GenerateJsonConfigDto] を持たない外部ライブラリの型は、DTO 上でもそのままの型として出力されます。これらをシリアライズするには別途 JsonConverter の登録が必要です。
ジェネレータによって出力されるクラスは以下の仕様を持ちます。
- 命名規則:
[モデル名]ForJsonという名前で生成されます。 - 名前空間: 元のモデルと 全く同じ名前空間 に生成されます。
- 物理構造: 全て
public partial classとして生成されます。partialであるため、ユーザー側で独自のメソッドやプロパティを追加して拡張することが可能です。
- オブジェクト識別子: 循環参照を解決するためのメタ情報として、すべての DTO クラスに自動的に
string? ___Idとstring? ___Refプロパティが追加されます。
親子で互いに参照し合っている場合でも、標準でシリアライズ可能です。内部的に ___Id と ___Ref トークンが発行され、デシリアライズ時に同一のインスタンスとして復元されます。
インターフェースや基底クラス(抽象クラス)をプロパティに持つ場合も、属性による識別により型情報を保持したまま保存できます。
基底型と派生型のそれぞれに適切な属性を付与します。
// 基底型: [GenerateJsonConfigDto] を付与
[GenerateJsonConfigDto]
public interface IEffect { }
// 派生型: [GenerateJsonConfigDto] と [JsonConfigDerivedType] を付与
[GenerateJsonConfigDto]
[JsonConfigDerivedType("Blur")]
public class BlurEffect : IEffect {
public ReactiveProperty<float> Radius { get; } = new(0f);
}Note
[JsonConfigDerivedType] に指定した文字列(型識別子)は、JSON 内の ___Type プロパティとして出力されます。
各派生型はアプリケーション起動時に ModuleInitializer を通じて自動的にレジストリへ登録されます。
ポリモーフィズムを有効にするには、JsonSerializerOptions に対して以下の設定(モディファイアの追加)が必要です。
var options = new JsonSerializerOptions() {
// 実行時レジストリに登録された派生型情報を追加する設定
TypeInfoResolver = MyJsonContext.Default.WithAddedModifier(ForJsonConverterRegistry.ApplyPolymorphism)
};これを行わない場合、___Type 識別子が JSON に出力されず、デシリアライズ時に実際の型を解決できなくなります。
生成される CreateModel メソッド内で子モデル(ネストされた ForJson 型)を処理する際、デフォルトでは引数として渡された IServiceProvider がそのまま引き継がれます。
これに対し、[JsonConfigCreateScope] 属性をプロパティに付与することで、そのプロパティのモデル生成時に毎回新しい DI スコープ (IServiceScope) を作成し、それを渡すように制御することが可能です。
[GenerateJsonConfigDto]
public class ParentModel {
// このプロパティの生成時のみ新しく DI スコープを生成する
[JsonConfigCreateScope]
public ChildModel ChildA { get; set; }
// こちらは sp がそのまま引き継がれる (デフォルト)
public ChildModel ChildB { get; set; }
}パフォーマンス上のオーバーヘッドを避けるため、デフォルトではスコープは作成されません。スコープ付きサービス (Scoped) を個別に生成・注入する必要があるプロパティに対してのみこの属性を適用してください。
ReactiveProperty<T> 以外の任意のラッパー型(例: Rx.NET の BehaviorSubject<T> や自作のラッパークラス)も、
[assembly: RegisterJsonConfigWrapper] 属性で登録することでジェネレータに認識させることができます。
IJsonConfigWrapper<TWrapper, TInner> を実装したオープンジェネリックのアダプタークラスを作成します。
using GenJsonConfig;
// 自作のラッパー型: MyBox<T> は .Content プロパティで内部値を保持するとする
public class MyBoxAdapter<T> : IJsonConfigWrapper<MyBox<T>, T> {
public T Get(MyBox<T> wrapper) => wrapper.Content;
public void Set(MyBox<T> wrapper, T value) => wrapper.Content = value;
}プロジェクト内の任意の .cs ファイルに assembly: 属性を記述します。
using GenJsonConfig.Attributes;
using MyApp; // MyBox<T> のある名前空間
[assembly: RegisterJsonConfigWrapper(typeof(MyBox<>), typeof(MyBoxAdapter<>))]これだけで、以後 MyBox<T> を使用するすべてのモデルプロパティが自動的に T? にマッピングされ、
MyBoxAdapter<T> 経由でアクセスコードが生成されます。
Note
[assembly: RegisterJsonConfigWrapper] は参照アセンブリ側に記述しても認識されます。
ライブラリとしてアダプターと登録をパッケージングし、利用側プロジェクトで参照するだけで有効になります。
Color 型のように、ジェネレータ側で DTO 化をサポートしていない型を扱う場合は、System.Text.Json 標準の JsonConverter を利用します。
// JsonSourceGenerationContext 等に Converter を登録
[JsonSourceGenerationOptions(Converters = [typeof(ColorJsonConverter)])]
[JsonSerializable(typeof(MySettingsForJson))]
public partial class MyJsonContext : JsonSerializerContext { }定義した MyJsonContext を使用して、リフレクションを最小化したシリアライズ・デシリアライズを行います。ポリモーフィズムを併用する場合は、オプションを介してコンテキストを生成します。
// 1. ポリモーフィズム設定を含んだオプションを作成
var options = new JsonSerializerOptions(MyJsonContext.Default.Options) {
TypeInfoResolver = MyJsonContext.Default.WithAddedModifier(ForJsonConverterRegistry.ApplyPolymorphism)
};
// 2. シリアライズの実行 (options を渡すことでモディファイアが適用される)
string json = JsonSerializer.Serialize(dto, options);
// 3. デシリアライズの実行
var loadedDto = JsonSerializer.Deserialize<MySettingsForJson>(json, options);[GenerateJsonConfigDto] を付与したクラスに対し、obj/ ディレクトリ配下に [モデル名]ForJson.g.cs という名前でソースコードが自動生成されます。
以下のようなドメインモデルを定義した場合の、生成コードの構造です。
[GenerateJsonConfigDto]
public class MySettings {
public ReactiveProperty<string> UserName { get; } = new("");
public string Theme { get; set; } = "Dark";
}// MySettingsForJson.g.cs (抜粋)
public partial class MySettingsForJson {
public string? ___Id { get; set; }
public string? ___Ref { get; set; }
public string? UserName { get; set; }
public string? Theme { get; set; }
// DTO からモデルへの変換
public static MySettings? CreateModel(MySettingsForJson? json, IServiceProvider sp, ReferenceResolver? resolver = null) {
if (json is null) return null;
if (json.___Ref is { } @ref) return resolver?.Resolve<MySettings>(@ref);
resolver ??= new ReferenceResolver();
var model = sp.GetRequiredService<MySettings>();
if (json.___Id is { } id) resolver.Add(id, model);
// 各プロパティの値を反映
if (json.UserName is { } v1) model.UserName.Value = v1;
if (json.Theme is { } v2) model.Theme = v2;
return model;
}
// モデルから DTO への変換
public static MySettingsForJson? CreateJson(MySettings? model, ReferenceTracker? tracker = null) {
if (model is null) return null;
tracker ??= new ReferenceTracker();
if (tracker.GetOrAddId(model) is { } refId) {
return new MySettingsForJson { ___Ref = refId };
}
return new MySettingsForJson {
___Id = tracker.GetId(model),
UserName = model.UserName.Value,
Theme = model.Theme
};
}
}- サポート対象:
classおよびinterfaceのみが対象です。recordやstructには非対応です。 - デシリアライズの要件: 通常のプロパティは
public setterを持っている必要があります。 - DI 前提:
CreateModelはIServiceProviderを利用してインスタンスを生成する設計になっています。
本プロジェクトは MIT ライセンスの下で公開されています。