OpenAPIとは
OpenAPI(旧 Swagger)というREST API用のインターフェイス定義方法をご存知でしょうか? OpenAPIに沿った定義ファイルを、Stoplight等を用いて書くと、openapi-generator-cli等のジェネレータを使うことで、Go/Python-Flaskなどのサーバーのテンプレート、Go/Python/TypeScript等のクライアントライブラリ、モックサーバー、ドキュメント等をある程度自動で生成させることができます。
ジェネレータを使うことで、一度定義ファイルを用意すれば、サーバー側、クライアント側で1から全部書かずに 自動生成したモデルを利用して割と迅速に実装を進めることができる、らしいです。
今回やろうとしたこと
相当時間をかけてOpenAPIのドキュメントを書いたので、可能な限り自動生成ベースでAPIサーバーを実装したい。
前バージョンはFlaskと生のSQLで割と闇なコードを書いて実装していたので、それをよりまとも(?)にリプレースするためGoとMongoDBで書き直す。そこで、リクエストされたモデルをバリデーションした上でMongoDBに直接保管したい。
使ったジェネレータ
openapi-generator-cliの go-server(net/http) ビルドした。ginやecho(oapi-gen)でもビルドできるようだが、goをはじめて使う自分的には、書く部分がなるべく少ないやつを使いたかったためnet/httpにした。Goに限って言えばGo-Swaggerというのが結構使えるようなのだが現在の定義ファイルの書き方OpenAPI(v3)になる前のSwagger(v2)にしか対応していないようなので、利用することができなかった。
使ったライブラリ
MongoDBオフィシャルのmongo-driverを使った。以前はmgoがかなりおすすめされていたようだが、既にメンテナンスが終わってしまっているので渋々これを使うことにした。
今回ジェネレートしたモデル
アカウントのデータモデルです。アカウント情報作成時にはこのモデルのフィールドをほぼ全部埋めたもの、アカウント情報更新時にはこのモデルの一部フィールドを埋めたものが渡されます。
MongoDBに登録するモデル
ジェネレートしたモデルで定義されているフィールドと別にMongoDBにだけ保存する 自動ID/Totp用のシークレットを入れたい。なのでこのモデルを埋込み型(継承的なもの)を使って MongoDB保存用のモデルを作成した。
MongoDBへの挿入
ユーザーという変数を作り、MongoDBに保存したいフィールドを適当に埋める。Insertするときは、Struct型をJSON文字列型に変換し、JSON文字列をBson型に変換することで、MongoDBに挿入できるようになる。(このとき_idフィールドがstringになってしまうので強引ではあるがbytesに戻してある) 元のジェネレートしたモデルに bson:フィールド名という指定がなくてもこれで挿入はうまく行った。
MongoDBからの取得
こっちで詰んでしまった。まず、取ってきたものを ジェネレートしたモデルを改変したものにデコードしようとしてみた。するとジェネレートしたモデルに追加した部分しかデータが入らなかった。おそらくジェネレートされた部分にbson:フィールド名の指定がなく、MongoDBから受け取ったフィールドをどこに割り当てていいかわからないってことなのだろう。(だからといってジェネレートされたモデルにbson:部を付けてもなぜか変換できなかったのだが)
次に、ジェネレートしたモデルそのものにデコードしようとしてみた。 すると(おそらく追加したフィールド分の余ってるデータが原因で?)cannot decode string into a boolean と言われてしまった。booleanを使う部分はジェネレートしたフィールド、追加したフィールドのどこにもないので完全に想定外の動作をさせてしまっているようだ。
ちなみにフィールドを追加せず、ジェネレートされたモデルそのままのドキュメントを挿入した場合はなぜかデコードできる。(bson:部がないのになぜかできてしまう。)
ちょっとあがいてみる
Decodeを使わず、一旦bson.Rawにデータを渡し、raw→Bsonモデル→BsonBytes→JsonBytes→Jsonモデルに変換しようとした。ジェネレートしたstructにbsonへの変換フィールドがないってことが原因なら、一旦JSONにしてしまえばJSONのマッピング方法はわかるんじゃないかという魂胆だ。
その結果のデータは空。というかたぶんそもそもbsonをjsonに変換するのができてない。
変換途中の出力では、Key/Valueのデータは確かに存在しているのだが、これをStructにマッピングする方法がどうしてもわからないようだ。フィールド名の1文字目が大文字になっているだけで全部同名のフィールドを付けているのだが...
とにかくこの状態から何かどうこうするのは相当めんどくさいようだった(reflectを使って1つずつ手動でマッピングすればなんとかできるのだろうが、とにかく大変そうである)
今回の結論
OpenAPI-generatorで生成したモデルを使って、そのままMongoDBに挿入/取得することは一応できる。だが、埋込み型(継承)を使ってフィールドを増やすと、挿入ができても取得したときに改変したモデルへのマッピングができなくて詰んでしまう。というか、たぶんMongoDBに直接挿入するのは想定していないらしいしやめたほうが良さそうである。素直にMongoDBに挿入するためだけのモデルを別途用意しましょう。