every Tech Blog

株式会社エブリーのTech Blogです。

swagとecho-swaggerを使ったSwagger UIでの開発談

はじめに

こんにちはMAMADAYSバックエンドチームのrymiyamotoです。最近エルデンリングを遊び倒しています。

MAMADAYSではアプリとWebで利用しているAPI(golang)の仕様をドキュメント化するためにSwaggerを利用しています。

導入をしてから3年以上経過したため、APIの開発運用を進める中で出てきた課題点への施策を綴っていこうと思います。

そもそもSwaggerとは?

SwaggerはOpenAPIというRESTful APIの仕様を記述するためのフォーマットを使用したツールで、仕様が文章化されることで開発者や関係者での認識が取りやすくなります。

動作環境

MAMADAYSではSwaggerの利用にあたって以下のツールを使っています。

ツール名 用途 バージョン
swag ドキュメントの自動生成 v1.8.0
echo-swagger Swagger UIの表示で利用(ドキュメントの可視化) v1.3.0

Swaggerをそのまま使う分にはyamlを表記するだけですが、MAMADAYSではドキュメントを自動生成するための swag を使っています。 swag では定義したstructの型に合わせてドキュメントを生成するのでyamlを直接手で変更する必要がなく楽です。

また生成されたドキュメントのままだと視覚的に分かりにくいため、Swagger UIを表示できるように echo-swagger を利用しています。

以下MAMADAYSので表記に合わせた簡易的な例です。 (goのバージョンは1.17.8です)

package main

import (
    "net/http"

    _ "github.com/rymiyamoto/swagger-test/docs"

    "github.com/labstack/echo/v4"
    echoSwagger "github.com/swaggo/echo-swagger"
)

type (
    Response struct {
        Int64  int64  `json:"int64"`
        String string `json:"string"`
        World  *Item  `json:"world"`
    }

    Item struct {
        Text string `json:"text"`
    }
)

// @title         example
// @version       1.0
// @license.name  rymiyamoto
// @BasePath      /
func main() {
    e := echo.New()

    e.GET("/swagger/*", echoSwagger.WrapHandler)
    e.GET("/", hello)

    e.Logger.Fatal(e.Start(":1323"))
}

// hello godoc
// @Summary  Hello World !
// @ID       HelloWorldIndex
// @Tags     HelloWorld
// @Produce  json
// @Success  200  {object}  Response
// @Router   / [get]
func hello(c echo.Context) error {
    return c.JSON(http.StatusOK, &Response{
        Int64:  1,
        String: "example",
        World: &Item{
            Text: "hello world !",
        },
    })
}

※go.modとgo.sumは省略しています

$ go install github.com/swaggo/swag/cmd/swag@v1.8.0
$ swag init
$ go mod tidy
$ go run main.go

NULL許容の値を表現する

sql.NullStringsql.NullInt64 などのNULL値を含むデータをそのまま使うことができないため swaggertype:"XXX" で対象のキーに表現したい型を定義するかと思います。

しかしこのままだとNULL許容であるかどうかがわかりません。

方法としては2種類あるので紹介します。

descriptionを追加する

対象のキーにコメントとして書くことでdescriptionが追加でき、ここでNULL許容であるかどうかを表現してます。

Hello struct {
    NullInt64  sql.NullInt64  `json:"null_int64" swaggertype:"integer"` // nullable
    NullString sql.NullString `json:"null_string" swaggertype:"string"` // nullable
}

extensionsで任意の追加情報を付与する(echo-swagger v1.3.0では非対応)

extensions:"x-XXX"で任意の追加情報を付与することが可能です。

NULL許容を表現するにあたっては extensions:"x-nullable" で指定することにします。

Add extension info to struct field

Hello struct {
    NullInt64  sql.NullInt64  `json:"null_int64" swaggertype:"integer" extensions:"x-nullable"`
    NullString sql.NullString `json:"null_string" swaggertype:"string" extensions:"x-nullable"`
}

ただし echo-swagger(v1.3.0) 上では表示できないため、出力されたyamlをSuwagger Editor上で確認する人向けです。

(表示がうまくいかないのは、依存packageである swaggo/files の内部で保持しているファイルが古そうです)

同一リソース名を扱う

同一リポジトリ内でAppやWeb・Dashboard等でAPIを作成している場合、リソース名が重複します。

このとき swag 側で全体のパスを含めた構造体名に変更してくれますが、その表記が長く冗長になってしまいます。

単純に1つしかSwaggerを利用しなければ気にすることはありませんが、表記が長くならないようにそれぞれprefixを足して見通しを良くしています。

// DashboardHoge 内部向けDashboard用
DashboardHoge struct {
    Text    string    `json:"text"`
    StartAT time.Time `json:"start_at"`
    EndAT   time.Time `json:"end_at"`
}

// Hoge App用
Hoge struct {
    Text string `json:"text"`
}

structでリクエストのbodyを表現する

bodyを扱う場合に各APIのコメントに以下のように記載すればよいです。

// post godoc
// @Summary  Hello World !
// @ID       HelloWorldPost
// @Tags     HelloWorld
// @Produce  json
// @Param    title        body      string  true   "タイトル"
// @Param    description  body      string  false  "説明"
// @Success  200          {object}  Response
// @Router   / [post]
func post(c echo.Context) error {
    // ...
}

しかしこの状態だと

  • パラメーターが増えると定義が面倒
  • リクエストの定義とコントローラーの定義を別階層で管理していると抜け漏れが発生しやすくなる
  • 同一のリクエストを使いまわしていると修正が冗長になってしまう

となってしまいます。

その対策として、bodyには定義しているformのstructを渡すようにしています。

こうすることで、form部分の修正のみで対応するAPIのbodyも一括で変更できるため管理が簡単になります。

// post godoc
// @Summary  Hello World !
// @ID       HelloWorldPost
// @Tags     HelloWorld
// @Produce  json
// @Param    body  body      Form     true  "request body"
// @Success  200   {object}  Response
// @Router   / [post]
func post(c echo.Context) error {
    // ...
}

Form struct {
    Title       string `json:"title"`       // require
    Description string `json:"description"` // option
}

終わりに

swag を使い続けて3年も経過すると色々と気になるところが出てくるので、利用ルールの制定多くなってきました。

理想を言えばその部分も定義できればより汎用性が出そうです。

しかし適期的にアップデート内容を確認してきましたが、少しずつOpen API 3.0の記法も使えるようになってきているのでしばらくは使っていこうと思います。

皆様良きSwaggerライフを!