【AI Shift Advent Calendar 2021】OpenAPI SpecificationをGoで使ってみます

こんにちは、Development Team の水谷です
本記事は AI Shift Advent Calendar 2021 の11日目の記事です

今回は、OpenAPI Specification 3YAMLからGoのサーバーを生成してみます

OpenAPI Specification とは

パスパラメーターやリクエストボディー、レスポンスのフォーマットなどのAPI仕様を、JSONYAMLで記載することで、クライアントやサーバーのテンプレートを生成でき、クライアント/サーバー間で、同じ型や仕様を共有しながら開発が可能になる仕組みです

例えば、APIの仕様変更が生じた際に、API仕様が記載されるJSONYAMLの変更から始めることで、職域間で共通認識をもちながら、開発を進めることが可能になります

具体的な流れ

  1. OpenAPI Specification をYAMLで記述
  2. YAMLからGoのAPIテンプレートを生成

OpenAPI Specification をYAMLで記述

OpenAPI Tools に、petstore.yamlというサンプルがありますが、今回は以下2点のシンプルなAPI仕様とします

  1. ユーザのリスト取得
  2. ユーザの登録
openapi: "3.0.0"
info:
  version: 1.0.0
  title: User API
paths:
  /users:
    get:
      tags: [Users]
      operationId: ListUsers
      parameters:
        - name: limit
          in: query
          description: limit
          required: true
          schema:
            type: integer
            format: int32
        - name: page
          in: query
          description: page
          required: true
          schema:
            type: integer
            format: int32
      responses:
        "200":
          description: user response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/user"
    post:
      tags: [Users]
      operationId: CreateUsers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NewUser"
      responses:
        "200":
          description: user response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/user"
components:
  schemas:
    user:
      allOf:
        - $ref: "#/components/schemas/NewUser"
        - required:
            - id
          properties:
            id:
              type: integer
              format: int64
    NewUser:
      required:
        - name
        - age
      properties:
        name:
          type: string
        age:
          type: number

YAMLからGoのAPIテンプレートを生成する

今回、テンプレート生成するために、deepmap/oapi-codegenというライブラリを利用します
Overviewを参考にし、インストールしていきましょう

go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
インストールしたら、oapi-codegen -h でオプションを確認できます、今回利用するオプションを以下で確認していきましょう。​ちなみに、オプション説明はUsing oapi-codegenにもあります
Usage of oapi-codegen:
  -generate string
    	Comma-separated list of code to generate; default "types,client,server,spec"
  -o string
    	Where to output generated code, stdout is default
  -package string
    	The package name for generated code
1. サーバーのテンプレートを生成する(server)
2. パラメータ各種の構造体を生成する(type)
今回は上記2点を行うので、1.は serverオプションで用いて、
2.のパラメータ各種、例えばリクエストパラメータやレスポンスボディーの型生成するオプションは typesで可能です
結果的に以下2つのコマンドで生成が可能になります
oapi-codegen -generate "types" -package oas oas.yaml > ./oas/types.gen.go
oapi-codegen -generate "server" -package oas oas.yaml > ./oas/server.gen.go

typesオプションについて

oas.yamlから生成したコード./oas/type.gen.goから一部抜粋します。
これはoas.yamlに記載した期待するパラメータを構造体にマッピングしたものです

EchoのBind機能で、クライアントから送られてきたリクエストボディーの値を生成した構造体にマッピングできたり、
逆にレスポンス時のフォーマットとして、活用することができます

// Package oas provides primitives to interact with the openapi HTTP API.
//
// Code generated by unknown module path version unknown version DO NOT EDIT.
package oas

// NewUser defines model for NewUser.
type NewUser struct {
    Age  float32 `json:"age"`
    Name string  `json:"name"`
}

// User defines model for user.
type User struct {
    // Embedded struct due to allOf(#/components/schemas/NewUser)
    NewUser `yaml:",inline"`
    // Embedded fields due to inline allOf schema
    Id int64 `json:"id"`
}

// ListUsersParams defines parameters for ListUsers.
type ListUsersParams struct {
    // limit
    Limit int32 `json:"limit"`

    // page
    Page int32 `json:"page"`
}

// CreateUsersJSONBody defines parameters for CreateUsers.
type CreateUsersJSONBody NewUser

// CreateUsersJSONRequestBody defines body for CreateUsers for application/json ContentType.
type CreateUsersJSONRequestBody CreateUsersJSONBody

serverオプションについて

serverオプションでは、oas.yamlで定義した仕様に基づいて、インタフェースが定義されます
そのインタフェース定義を、実装で満たした構造体を用意することで利用ができます

今回、生成されたインタフェースは、ListUsersCreateUsersが定義されたServerInterfaceとなります

// ServerInterface represents all server handlers.
type ServerInterface interface {

    // (GET /users)
    ListUsers(ctx echo.Context, params ListUsersParams) error

    // (POST /users)
     CreateUsers(ctx echo.Context) error
}

このServerInterfaceを満たす構造体(handler)を用意し、README.mdのRegistering Handlersを参考にしつつ、サーバを起動してみましょう

package main

// ServerInterface の定義を満たす構造体
type server struct{}

// ListUsers は ServerInterface に定義される ListUsers の実装
* func (h server) ListUsers(ctx echo.Context, params oas.ListUsersParams) error {os.Stdout
    return ctx.JSON(200, map[string]interface{}{
        "msg":   "200 success",
        "limit": params.Limit,
        "page":  params.Page,
    })
}

// CreateUsers は ServerInterface に定義される CreateUsers の実装
func (h server) CreateUsers(ctx echo.Context) error {
    b := &oas.CreateUsersJSONBody{}
    if err := ctx.Bind(b); err != nil {
        return err
    }

    return ctx.JSON(
        200,
        map[string]interface{}{
            "msg":     "200 success",
            "reqBody": b,
        })

}

// ServerInterface の定義を満たす server をルーティングする
// https://github.com/deepmap/oapi-codegen#registering-handlers の Echo の箇所を参考にします
func setHandler() *echo.Echo {
    s := server{}
    e := echo.New()

    // RegisterHandlers の第2引数の型は ServerInterface なので、
    // ServerInterface を満たした構造体を代入する
    oas.RegisterHandlers(e, s)

    return e
}

func main() {
    e := setHandler()
    e.Logger.Fatal(e.Start("0.0.0.0:8080"))
}
$ go run main.go

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.6.1
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080

リクエストを送ってみる

ブラウザからhttp://localhost:8080/usersにアクセスすると、requiredなクエリパラメータが不足しているので、400が返ります

{
  "message": "Invalid format for parameter page: query parameter 'page' is required"
}

今回必要なクエリパラメーターは,pagelimitなので、それを付与したURL(http://localhost:8080/users?page=1&limit=1)にアクセスすると、期待通り200が返ります

{
  "msg": "200 success",
  "limit": 1,
  "page": 10
}

一方で、POSTのリクエストについては、NewUserの構造体を参考にJSONボディーを用意しPOSTすると、これも正しく200が返ってくるかと思います。

curl -X POST 'localhost:8080/users' \
-H 'Content-Type: application/json' \
-d '{
  "age": 20,
  "name": "taro"
}'
{
   "msg":"200 success",
   "reqBody":{
      "age":20,
      "name":"taro"
   }
}

例えば、ageの値を20から"20"にすると、期待しているageの型がstringではなく数値である旨のエラーメッセージと共に400が返ります

このようにして型安全なAPIを開発することが可能になります

{
  "message": "Unmarshal type error: expected=float32, got=string, field=age, offset=15"
}

終わりに

​今回はcurlで疎通を試みましたが、 APIクライアントの生成ができる点も、OpenAPI Specificationのメリットです

OpenAPITools/openapi-generatorというライブラリを使えば、TypescriptやGo、その他の言語でのAPIクライアント生成が可能なので、気になる方は是非使ってみてください!