ひでぼ~blog

C#ときどきゲーム

検証属性付きのrecordからswagger.jsonを生成するときの悩み

ASP.NET Coreでリクエストパラメータにrecordを使った場合に出力されるswagger.jsonにRequired等の属性が反映されない問題があったのでまとめました。 問題になっているのはだいたい次のIssueと同じ内容です。

github.com

最近はエンドポイントのリクエストパラメータにrecordを使うことが多く、次のようなパラメータを作ってASP.NET Coreのエンドポイントで受け取っています。

public record TodoCreateParameters([Required] string Description, [Required] DateTime DateTime)

これを次のようなアクションで受け取ります。

[HttpPost]
public IActionResult CreateTodo([FromBody] TodoCreateParameters parameters)
{
    // 永続化層への登録処理など

    return Created();
}

これでデバッグ実行してエンドポイントを叩いてみると、パラメータがnullのときにバリデーションが効いて400BadRequestが返りうまくいっているように見えるのですが、 swagger.jsonを確認してみると、

  "components": {
    "schemas": {
      "TodoCreateParameters": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "nullable": true
          },
          "dateTime": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  }

TodoCreateParametersのdescriptionとdateTimeがrequiredになっていません。

なので属性のAttributeTargetsをpropertyと明示するようにしました。

public record TodoCreateParameters([property: Required] string Description, [property: Required] DateTime DateTime)

すると生成されるswagger.jsonはrequiredがつくようになりました。

  "components": {
    "schemas": {
      "TodoCreateParameters": {
        "required": [
          "dateTime",
          "description"
        ],
        "type": "object",
        "properties": {
          "description": {
            "minLength": 1,
            "type": "string"
          },
          "dateTime": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  }

ただ、ここでエンドポイントを叩いてみるとエラーになります。

System.InvalidOperationException: Record type 'PrimaryCtorValidationSample.TodoCreateParameters' has validation metadata defined on property 'DateTime' that will be ignored. 'DateTime' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter.

プライマリコンストラクタの引数はコンストラクタの引数とプロパティを兼ねているので、どっちにつける属性なのかハッキリしてほしいみたいな内容だと思います。 エラーメッセージに従って、[property: Required]を[param: Required]に変えてみました。すると今度は実行時エラーは出なくなりましたがswagger.jsonにrequiredが反映されなくなりました…。

良い解決策が見つからず妥協して次のようにrecordを作りました。

public record TodoCreateParameters
{
    public TodoCreateParameters(string description, DateTime dateTime)
    {
        Description = description;
        DateTime = dateTime;
    }

    [Required]
    public string Description { get; init; }

    [Required]
    public DateTime DateTime { get; init; }
}

プロパティとコンストラクタが簡潔に書けるrecordの良いところが無くなってますがこれでやりたいことはできるようになりました。 ただこれだとReSharperがプライマリーコンストラクタを使うように波線を出して提案してくるのが少しウザいです…。