Golang+Ginでswaggerを自動生成する(gin-swagger)


はじめに

api開発の時に必須と言っても過言ではないswaggerですが、なんと言っても保守し続けるのがとても面倒くさい、、、
できればコードから勝手にswaggerを生成してくれるといいのですが、、、そんな要望に対しての解決策の1つとなるのがこの

gin-swagger(https://github.com/swaggo/gin-swagger)

です。これはgoのコードに指定のコメントを追加していくことで自動的にswaggerのコードを作成していくためのツールです。

今回のゴール

今回はこのような簡単なCRUDのswaggerをgin-swaggerを用いて作成していきたいと思います!

ディレクトリツリーはこんな感じです!

rootディレクトリはgin-swagger-testとしています

gin-swagger-test
.
├── docs # ←自動生成されるディレクトリ
│   ├── docs.go 
│   ├── swagger.json
│   └── swagger.yaml
├── go.mod
├── go.sum
├── handlers
│   └── todo.go
├── main.go
├── models
│   └── todo.go
└── responses
    ├── errors.go
    └── success.go

セットアップ

まずはセットアップです

$ go get -u github.com/swaggo/swag/cmd/swag
$ go get -u github.com/swaggo/gin-swagger
$ go get -u github.com/swaggo/files

# 下記コマンド実行後root配下に`docs`ディレクトリが作成される
$ swag init ./main.go

# もし`swag not found`的な警告が出た場合は下記コマンドを実行し改めて `swag init`を行う
$ export PATH=$(go env GOPATH)/bin:$PATH

参考文献:https://github.com/swaggo/swag/issues/197

main.go
package main

import (
    "github.com/gin-gonic/gin"

    // docsのディレクトリを指定
    _ "gin-swagger-test/docs" // ←追記

    ginSwagger "github.com/swaggo/gin-swagger" // ←追記
    "github.com/swaggo/gin-swagger/swaggerFiles" // ←追記
)

func main() {
    r := gin.Default()

    // 下記を追記することで`http://localhost:8080/swagger/index.html`を叩くことでswagger uiを開くことができる
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    r.Run()
}

ベースのアノテーション

まずはこの辺りの記述方法です

もっとたくさんのアノテーションがありますが、最低限必要なのは

  • title
  • version
  • license.name

の3つのみです
より詳細なアノテーションをつけたい場合はこちらを参照してください

main.go
// @title gin-swagger todos 
// @version 1.0
// @license.name kosuke
// @description このswaggerはgin-swaggerの見本apiです
func main() {
    r := setupRouter()
    r.Run()
}

handlerアノテーション

Getリクエスト

handlers/todo.go
// GetTodos ...                                                        
// @Summary Todo一覧を配列で返す                                          -> ①どのようなエンドポイントなのかを端的に示すコメント
// @Tags Todo                                                          -> ②tagの指定、tagベースでエンドポイントをグループ化する時に役立つ
// @Produce  json                                                      -> ③どのような形のデータを返すかを指定
// @Success 200 {object} responses.SuccessResponse{data=[]models.Todo} -> ④successレスポンス
// @Failure 400 {object} responses.ErrorResponse                       -> ⑤errorレスポンス
// @Router /todos [get]                                                -> ⑥ルーティング
func (t *Todo) GetTodos(c *gin.Context) {
    todos, err := models.PostTodo() // ←何かしらのTodo配列を返す処理
    if err != nil {
        // エラーハンドリング
    }
    c.JSON(200, todos)
}

基本的にswaggerを書いたことのある人であれば「なるほどなぁ〜」となる内容だとは思うのですが、1点よくわからないところがあるとするなら

// @Success 200 {object} responses.SuccessResponse{data=[]models.Todo} -> ④successレスポンス
// @Failure 400 {object} responses.ErrorResponse                       -> ⑤errorレスポンス

上記のあたりでしょうか?
successレスポンス、errorレスポンスはそれぞれ
@Success ステータスコード 返り値の型 値
と言うような引数をとります。

また、todo配列のjsonを返したい場合はレスポンス用のstructを用意しそこに対して
responses.SuccessResponse{data=[]models.Todo}
と言うような形で配列のtodoを代入することで実現します

responses/success.go
// SuccessResponse ...
type SuccessResponse struct {
    Data interface{} `json: "data"`
}
models/todo.go
// Todo ...
type Todo struct {
    ID    int    `json:"id" example:"1"`
    Title string `json:"title" example:"title1"`
    Body  string `json:"body" example:"body1"`
}
responses/errors.go
// ErrorResponse ...
type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

Postリクエスト

handlers/todo.go
// PostTodo ...
// @Summary 新規Todoを作成
// @Tags Todo
// @Accept  json                         -> ①受け取るデータ型を指定
// @Produce  json
// @Param title body string true "title" -> ②受け取るパラメータを指定
// @Param body body string true "body"
// @Success 201 {object} responses.SuccessResponse{data=models.Todo}
// @Failure 400 {object} responses.ErrorResponse
// @Router /todos [post]
func (t *Todo) PostTodo(c *gin.Context) {
    todos, err := models.PostTodo() // ←何かしらの新規Todoを作成する処理
    if err != nil {
        c.JSON(400, &responses.ErrorResponse{Code: 400, Message: "bad request"})
    }
    c.JSON(200, todos)
}

postリクエストでつまずくところは

// @Param title body string true "title" -> ②受け取るパラメータを指定
// @Param body body string true "body"

あたりのパラメータの指定方法だと思いますこれは

@Param パラメータ名 パラメータの場所(header, body...) データ型 必須項目か? コメント
と言うような引数を与えることでパラメータを指定しています

Patchリクエスト

handlers/todo.go
// PatchTodo ...
// @Summary 既存Todoを更新
// @Tags Todo
// @Accept  json
// @Produce  json
// @Param id path int true "id"
// @Param title body string false "title"
// @Param body body string false "body"
// @Success 200 {object} responses.SuccessResponse{data=models.Todo}
// @Failure 400 {object} responses.ErrorResponse
// @Router /todos/{id} [patch]
func (t *Todo) PatchTodo(c *gin.Context) {
    todos, err := models.PatchTodo() // ←何かしらのTodoを更新する作成する処理
    if err != nil {
        c.JSON(400, &responses.ErrorResponse{Code: 400, Message: "bad request"})
    }
    c.JSON(200, todos)
}

Deleteリクエスト

handlers/todo.go
// DeleteTodo ...
// @Summary 既存Todoを削除
// @Tags Todo
// @Accept  json
// @Produce  json
// @Param id path int true "id"
// @Success 201 {object} responses.SuccessResponse{data=models.Todo}
// @Failure 400 {object} responses.ErrorResponse
// @Router /todos/{id} [delete]
func (t *Todo) DeleteTodo(c *gin.Context) {
    todos, err := models.DeleteTodo() // ←何かしらのTodoを削除する処理
    if err != nil {
        c.JSON(400, &responses.ErrorResponse{Code: 400, Message: "bad request"})
    }
    c.JSON(200, todos)
}

以上です。
アノテーションを修正後は改めて

$ swag init ./main.go

とすることで既存のswaggerを上書きしてくれます

まとめ

swaggerを自動生成するためにgin-swagger(swag)の書き方を覚えるのは多少めんどくさい部分があるのは否めないですが、自動生成をしてあげることで

  • 書き方の統一感を出せる
  • ドキュメントと実装の乖離する可能性を多少防げる

と言うメリットは確実にあるので
試しに採用する価値はあると思います。