最近のVisual Studioでは jsonの文字列をコピーして、C# のクラスとして貼り付ける機能があります。
なんですが、これ元のjsonのキーが snake_case の場合、できたC#のクラス名も snake_case になるので気持ち悪いんですね。
C# でjsonを扱う多くの場合、Json.Net (Newtonsoft.Json) を利用していると思います。このJson.Netを使う場合、JsonPropertyという属性をプロパティに指定すると、任意のプロパティ名をC#側で利用できて、PropertyNameに指定した名前をjsonのキーとして利用することができるようになります。しかし、すべてのプロパティに手動でJsonPropertyを追加するのはハイパー刺身たんぽぽ作業でつらい...
というわけで、プロパティ名をPascal Caseに変更する CodeFixProvider を作りました。Nugetから利用できます。
まだ、とりあえず作ってみた状態でいくつか既知の問題があります。他にも見つけたらぜひバグレポートをお願いします。
- Json.Netのインストールは行わない(存在しないとコンパイルエラーになる)
- usingディレクティブが1つもないコードには using Newtonsoft.Json の自動追加ができない
- 繰り返し実行することは想定していない
コードとしては CodeFixProvider.cs がほぼすべてですが、3点ほど詳しく取り上げてみます。
- 繰り返し SyntaxNode の置き換えをやる場合、その都度置換対象のSyntaxNodeを取得しないといけない
- プロパティに属性を追加するコード
- usingディレクティブに Newtonsoft.Json を追加するコード
繰り返し SyntaxNode の置き換えをやる場合、その都度置換対象のSyntaxNodeを取得しないといけない
今回、クラスのプロパティすべてに対して繰り返し操作を行いますが、SyntaxTree は immutable なのでその都度作り直します。なので、あらかじめクラスのプロパティ一覧を IEnumerable
var root = await document.GetSyntaxRootAsync(cancellationToken); var newRoot = root; var names = typeDecl.ChildNodes() .OfType<PropertyDeclarationSyntax>() .Select(p => p.Identifier.Text); foreach (var name in names) { //最新のドキュメントルートから変更対象のプロパティを名前から探す。 //ドキュメントルートは構築しなおされているので、SyntaxNodeの参照一致では見つけられない var property = newRoot.DescendantNodes() .OfType<TypeDeclarationSyntax>() .First(t => t.Identifier.Text == typeDecl.Identifier.Text) .ChildNodes() .OfType<PropertyDeclarationSyntax>() .First(p => p.Identifier.Text == name); var previousName = property.Identifier.Text; //以下省略 }
プロパティに属性を追加するコード
これはコードを見るのが早いでしょう。[JsonProperty(snake_case, Hoge = 1)]
というプロパティを構築するコードです*1。
//新しいプロパティを構築 var newProperty = property .WithIdentifier(SyntaxFactory.Identifier("snake_case")) .AddAttributeLists( SyntaxFactory.AttributeList( SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Attribute(SyntaxFactory.ParseName("JsonProperty")) .AddArgumentListArguments( SyntaxFactory.AttributeArgument( SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(previousName))), SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Hoge"), default(NameColonSyntax), SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(1))) ) ) ) );
usingディレクティブに Newtonsoft.Json を追加するコード
usingディレクティブをチェックして、Newtonsoft.Jsonが定義されていない場合のみ末尾に追加するコードです。TODOコメントにあるように、usingが1つもないときに挿入する方法がわかっていません...
if (newRoot.ChildNodes() .OfType<UsingDirectiveSyntax>() .All(u => (u.Name as QualifiedNameSyntax)?.ToFullString() != "Newtonsoft.Json")) { //usingディレクティブの最後の要素 var lastUsing = newRoot.DescendantNodes() .OfType<UsingDirectiveSyntax>() .LastOrDefault(); var insertingUsing = SyntaxFactory.UsingDirective( SyntaxFactory.IdentifierName("Newtonsoft.Json")); if (lastUsing == null) { //TODO using が1つもない場合に挿入する方法がわからない //newRoot = newRoot.InsertNodesBefore(newRoot.ChildNodes().First(), new[] {insertingUsing}); } else { newRoot = newRoot.InsertNodesAfter(lastUsing, new[] { insertingUsing }); } }
CodeFixProvider は面白いんですが、「こうしたいときにこう書く」という情報が探しづらいので、もっと出てくるといいですね。
*1:もっと Parse を使って簡単にかける気もしつつ