🥺

Swagger (OpenAPI 2.x) generates go-server codeの何が辛いのか

2020/10/18に公開
2

注意

  • まだ書くべきことはたくさんある
  • go-swaggerは駄目だ、と見切りをつけて数年が経つので、情報が古い可能性がある
  • OpenAPI 3.x を否定するものではない(むしろそちらには若干の希望を感じている)
  • 解決策は書いてない

結論

  • Swagger generates go-server codeを採用するには、相当な覚悟が必要
  • どうせやるならOpenAPI 3.xに賭けたほうがいい

背景

この記事を書こうと思った動機

とつぶやいたものの、そのデメリットについて明確な言語化したことがなく、 単なる言いがかりじゃない というところをきちんとしておくため。

Swaggerはつらいよ

実際にプロジェクトで採用してみた結果、一体何が辛かったのかについて、3つの点から解説する。

問題点1: Swaggerの表現力と戦わねばならない

そもそも$refがキツイ

説明まで含めて完全に同一な定義なんてそう多くはない。

definitions:
  dateRange:
    description: 日付の範囲
    type: object
    properties:
      start:
        type: string
	format: date
      end:
        type: string
	format: date
  user:
    contractPeriod:
      $ref: '#/definitions/dateRange'
      description: 契約期間 # こうは書けない
  schedule:
    reservablePeriod:
      $ref: '#/definitions/dateRange'
      description: 予約枠の期間 # こうは書けない

プロパティ名から察してくださいね、となる。
もちろん良いプロパティ名をつけることにネガティブはないけれども、生成されたドキュメントを見て

## user
### user. contractPeriod
日付の範囲
## schedule
### schedule.reservablePeriod
日付の範囲

と書かれてて嬉しいケースなんてあるんだろうか。
本来、swaggerはわかりやすいドキュメンテーションができること、が主眼のはず。
コードを生成しないのであれば、定義を分けてしまえばいい。

definitions:
  user:
    contractPeriod:
      description: 契約期間
      type: object
      properties:
        start:
          type: string
          format: date
        end:
          type: string
	  format: date
  schedule:
    reservablePeriod:
      description: 予約枠の期間
      type: object
      properties:
        start:
          type: string
          format: date
        end:
          type: string
	  format: date
## user
### user. contractPeriod
契約期間
## schedule
## schedule.reservablePeriod
予約枠の期間

当然この場合、コードを生成したとき contractPeriodreservablePeriod はそれぞれ単なる *struct (たまたま同じメンバーを持つ)である。

type DateRange struct {...} なんてのは幻想となる。
さて、どっちを取る?

モデルの定義を共有しづらい

複数のAPIサーバーの定義を書くと、モデルの定義が共有できないことに苦しめられる。

あるサービスのバックエンドとして、次のような複数のAPIサーバーで構成していた。
(※飽くまで例として、抜粋したり名前や本来の機能とは違う説明にしている)

  • User API: エンドユーザーが触れる機能を提供するAPIサーバー
  • Supporter API: サポート担当者が触れる機能を提供するAPIサーバー
  • Manager API: バッチ等がツール内部のデータを取得/操作したりするためのAPIサーバー

肝となるのは、

  • 複数のAPIサーバーで構成される
  • それぞれのAPIの 背景にあるモデル定義は(ほぼ)共通
  • それぞれのAPIの 提供する機能は少しずつ違う

というところ。

それぞれのAPI定義を生成するためには

$ go-swagger ./user-api.yaml ...
$ go-swagger ./supporter-api.yaml ...
$ go-swagger ./manager-api.yaml ...

とするわけだが、当然それぞれのモデル定義はバラバラに作られる。
結果次のような既存のモデルなどからそれぞれのモデルを作るため、次のようなコードを量産するはめになる。

package userapi

import (
	"github.com/kyoh86/foo/swagger/gen/user/restapi/def"
)

func BuildUserResponse(name string) *def.User {
	return &def.User{
		Name: &name,
	}
}
package supporterapi

import (
	"github.com/kyoh86/foo/swagger/gen/supporter/restapi/def"
)

func BuildUserResponse(name string) *def.User {
	return &def.User{
		Name: &name,
	}
}

実に有意義なコード♡

  • Q: これくらいだったら、生成コードに含めてしまえば良いのでは?
    • 飽くまでここに書いた例(BuildUserResponse)が小さいからそう見えるだけ
  • Q: goのstructは、同じ型・同じメンバーなら逐一書かなくても相互に変換できるけど?
  • Q: サーバーを分けなければ良いのでは?
    • そうですね

これがあまりにも耐えられず、現在は gen/common/def にモデルを生成し、
その def を参照するように各 restapi を吐き出す、という仕様にカスタマイズしている。
このときのカスタマイズがあまりにも大掛かりだったので、未だに4年前のあのPull-requestのタイトル ── 大統一def ── を覚えている。物理学会も騒然である。ありがとうM氏。

そして、結果としてこのときのカスタマイズが今も大きな負債としてプロジェクトにのしかかっている。当時あれを意思決定した自分を○しに行きたい。タイムマシンはよ

その他

気が向いたら詳細を書くかも:

  • requiredなのにポインタ
    • https://github.com/wacul/ptr 大活躍(全然うれしくない)
    • ちょっと油断するとすぐ nil dereference で死ィ
    • テスト書くときの苦痛たるや
  • 組み合わせを指定できないvalidation
    • このvalidationどこでやってんだっけ?が統一できない苦しさ
    • validationをyaml(JSON)で表現する事自体によるものなのでswaggerは関係ないけど
  • example
    • descriptionに同じく。$refしたらexampleは書けない
    • Mock Serverの自動生成でClientが叩き放題ウッハウハ!なんてのは 幻想
      • ここはOpenAPI 3.0で少し改善した
  • そもそも表現力…
    • 表現力はGoのコード >>>>> yamlであることは疑う余地がないと思う
    • 表現力の小さいもので、より表現力の高いものを書き下そうってのは結構変な話だよね

問題点2: 知見が少ない(少なかった)

  • 「触ってみた」という記事は数多
  • 本格的に大規模プロジェクトで採用して苦しんだ、こう乗り越えたという記事は ほぼ ない(なかった)
  • プロダクトの内部事情を上手く取り除いて知見化するのが難しいからかも

問題点3: 問題の多いgo-swaggerプロジェクト

前提: カスタムでgo-swaggerのフォークに手を染めがち

先程の 大統一def もさることながら、go-swaggerのカスタマイズ性の限界を突破してしまうことがままある。
しかし、 forkだけは絶対やるべきではない 。やるならば、(カスタマイズしたgo-swaggerを採用した)プロジェクトに生涯付きそう覚悟本家go-swaggerに追いつけなくなったら腹を切る覚悟をしてから。
そしてそんな覚悟をするエンジニアは どっかイカれてる のでそれはそれで問題。
つまるとこ、 絶対本家をフォークすんな ということである。
やってみないとわからない、ダメ、ゼッタイ!

free-riderがとにかく多い

ちょっと触ってみてこれいいじゃん!と甘い覚悟のまま使い、Issueを立てる輩が後を絶たない。
JSON Schmaくらいは理解してから言え といったテイストの要望が非常に多い。
下手をすると JSONってご存知ですか? とCloseされてもおかしくないようなレベルのものも。
Pull-Requestですらそうなのだから、絶対に関わりたくないプロジェクトの一つである。
Contributorが疲弊しているのがよく見える。

コード生成という処理とGoの相性の悪さ(未執筆)

go-swaggerがGo製なのは失敗かも

Swaggerより愛を込めて(未執筆)

もしどうしてもSwaggerに期待して、上手く活用する道を探すなら

  • server to swagger (逆の生成): swaggo…ただ多分これもこれで茨の道
  • model, validatorだけでいいかも: それならJSON Schemaで十分なのでは…
  • OpenAPI 3.0
    • +1: 結構良い。だいぶ整理されてる。
    • +1: openapi-generatorも、go-swaggerを見捨てただけのことはある。
    • -1: 未だに$refは使いづらい
    • -1: keyをdef-nameとして利用する悪い習慣はJSON Schema譲りなので変わらない
      • {"<def-name>": <def-values>...} ってのがたまにある
      • {"name": "def-name", <def-values>...} に統一したほうが良いと思う
    • -1: いい加減、 ハナっから複数ファイルで定義する 仕様にしたほうが良いのでは
      • 実用的に見て単一のファイルで仕様を管理しきれるわけがない
      • $refで外部のファイルを参照できるから良いでしょ、なんて断片化したファイルをあっちこっちおかせて、結果、JSONのLSPやYAMLのLSPがSchemaを利用できずに上手く働かないというこの悲しさ

Discussion

utamoriutamori

つらみですね……
Stoplight Studioで、Specの記述は楽になったので
コード生成(コードからSpec、Specからコード)には手を出さず、
モックサーバーと契約テストでスキーマ駆動開発、ドキュメンテーションに絞った活用がいいかなぁ と思いました

kyoh86kyoh86

私も、コード生成は茨の道ゆえ、気を付けたほうがいいぞ、と思っています。
一見するととても楽で理想的なのですが、ライトユースでは気づきにくい深いところに沼があるのだ、ということを言いたくて。

モックサーバーと契約テスト
ドキュメンテーションに絞った活用
その3つ、おっしゃるとおりだと思うので、その道もいずれは探って記事にしてみたいですねぇ