KiotaでOpenAPIの定義からGoのクライアントを生成してみる

こんにちは。 kimihiro_nです。

Microsoftから「Kiota」というOpenAPIの定義からクライアントコードを生成するツールが公開されていたのでちょっと触ってみました。

learn.microsoft.com

Kiota の特徴

JSON、YAMLで書かれたOpenAPIのAPI仕様から、APIを呼び出すためのクライアント部分を自動生成してくれるツールです。 特徴としてはGoやPythonなど様々な言語への書き出しに対応していて、似たようなインターフェースで扱える点になります。 API仕様を一度共通の内部的なモデルに変換し、そこから各言語のクライアントを生成する面白いアプローチを取っています。

似たようなツールだとOpenAPI Generatorという有名なものがありますが、 ツール自体が巨大になってきてしまっているのと、CLIからだとGo言語でクライアントのみのコードを生成する方法が分からなかった(サーバー側のコードも一緒に生成されてしまう)ため、クライアントに特化したKiotaを試してみることにしました。

Kiotaを使ってみる

インストール

learn.microsoft.com

インストール手順はこちら。 コミュニティ作成によるものですがbrewによるインストールもできます。

OpenAPI の用意

クライアントを作るOpenAPI仕様を用意します。

swagger: "2.0"
basePath: /api/v1
definitions:
  main.ErrResponse:
    properties:
      detail:
        type: string
    type: object
  main.Post:
    properties:
      content:
        type: string
      id:
        type: integer
      title:
        type: string
    required:
    - content
    - title
    type: object
  main.PostListResponse:
    properties:
      posts:
        items:
          $ref: '#/definitions/main.Post'
        type: array
    type: object
info:
  contact: {}
  title: ExampleWebAPI
  version: "1.0"
paths:
  /posts:
    get:
      consumes:
      - application/json
      description: get posts
      parameters:
      - description: limit
        in: query
        name: limit
        type: integer
      - description: offset
        in: query
        name: offset
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: OK
          schema:
            $ref: '#/definitions/main.PostListResponse'
        "500":
          description: internal server error
          schema:
            $ref: '#/definitions/main.ErrResponse'
      summary: Get List of Posts
      tags:
      - posts
    post:
      consumes:
      - application/json
      description: add post
      parameters:
      - description: Post
        in: body
        name: post
        required: true
        schema:
          $ref: '#/definitions/main.Post'
      produces:
      - application/json
      responses:
        "201":
          description: created
          schema:
            $ref: '#/definitions/main.Post'
        "400":
          description: invalid params
          schema:
            $ref: '#/definitions/main.ErrResponse'
        "500":
          description: internal server error
          schema:
            $ref: '#/definitions/main.ErrResponse'
      summary: Create new post
      tags:
      - posts

投稿の一覧取得と投稿が出来る簡単なAPIです。 諸事情(後述)でOpenAPI 3.xではなくてOpenAPI 2.0の仕様に合わせて作っています。 2.0の方はSwaggerとも呼ばれてます。

Goクライアントの生成

こちらからKiotaのGoクライアントを生成してみます。

kiota generate -l go -d ./swagger.yaml -o ./client -n ${レポジトリ名}/client

-l オプションでGo言語を、-d オプションでOpenAPI(Swagger)ファイルの置き場を、-o で出力先ディレクトリを、 そして -n で生成されるクライアントのフルパッケージ名を指定します。 フルパッケージ名は生成されたコードのimportが正しく動くために必要で、go mod init したときのレポジトリ名 + 出力時のディレクトリ(/client)を指定します。

生成されたファイル

実行するとこのようなファイルが生成されます。 これらのファイルは触らなくて大丈夫ですがimportのエラーが出ている場合は-nオプションに渡している値が適切かどうかを確認してみてください。

Goクライアントを使ってみる

まずはAPIクライアントを初期化から。

package main

...
import "github.com/microsoft/kiota-abstractions-go/authentication"
import "github.com/microsoft/kiota-http-go"
import ${レポジトリ名}/client

func main()
    ctx := context.Background()
    authProvider := authentication.AnonymousAuthenticationProvider{}
    adapter, err := http.NewNetHttpRequestAdapter(&authProvider)
    if err != nil {
        log.Fatalf("Error creating request adapter: %v\n", err)
    }
        // サーバーのURLをセット
    adapter.SetBaseUrl("http://localhost:8080/api/v1")
    apiClient := client.NewApiClient(adapter)

API認証が必要なケースに対応するためのauthProviderを生成し、そこから通信用のadapterを作っています。 今回は認証不要のAPIを叩くのでAnonymousAuthenticationProviderを利用しました。 adapterはAPI通信部分を吸収する部分で、ここで独自のHTTP通信クライアントを組み込んだりも出来ます。 OpenAPIにサーバーのエンドポイントの記載がない場合はこのようにadapterで指定します。 adapterからクライアントを生成したら準備完了です。

limit := int32(3)
params := posts.PostsRequestBuilderGetQueryParameters{
    Limit: &limit,
}
// リクエストを送信
result, err := apiClient.Posts().Get(ctx, &posts.PostsRequestBuilderGetRequestConfiguration{
    QueryParameters: &params,
})

リクエストを組み立ててAPIを呼び出す部分のコードはこのような形です。 APIごとに専用の構造体が生成されているのでそれを利用して組み立てる形になります。 パラメータが未指定の場合と区別するためすべてポインタ型で渡すようなインターフェースになっています。 数値から直接ポインタを取ることはできないので、一度代入が必要な点は少し手間になります。 lo.ToPtr みたいな関数を利用してもいいかもしれません。

result, err := apiClient.Posts().Get(/* 省略 */)

if err != nil {
    // OpenAPIで定義されているエラーは errors.As でキャスト出来る
    var errResp *models.ErrResponse
    if errors.As(err, &errResp) {
        fmt.Printf("request error: %s\n", *errResp.GetDetail())
        return
    }
    fmt.Printf("Error getting inference result: %+v\n", err)
    return
}
// Post の一覧を出力
for _, post := range result.GetPosts() {
    fmt.Printf("id: %d, title: %s, content: %s\n", *post.GetId(), *post.GetTitle(), *post.GetContent())
}

/* 実行結果例
id: 1, title: First Post, content: This is first test post.
id: 2, title: Second Post, content: This is second test post.
id: 3, title: Third Post, content: This is third test post.
*/

リクエストを行うと、パースされたレスポンスとエラーにアクセスすることが出来ます。 エラーはOpenAPIに定義されていれば専用のモデルが生成されるので、キャストすることでエラーオブジェクトにもアクセスすることが可能です。 定義されていないエラーの場合は汎用的なエラーが返ってきます。 正常時のレスポンスもパースされた状態で入っておりGetter経由で好きに取り出すことが出来ます。 JSONのパースなどを自分で記述しなくていいのはコード生成の大きなメリットです。

// リクエストデータの作成
requestData := models.NewPost()
title := "Hello world"
requestData.SetTitle(&title)
content := "This is a test from kiota apiClient."

// リクエストに追加
requestData.SetContent(&content)

// リクエストを送信
result, err := apiClient.Posts().Post(ctx, requestData, &posts.PostsRequestBuilderPostRequestConfiguration{
    Headers: header,
    Options: []abstractions.RequestOption{
        kiotaHttp.NewCompressionOptions(false),  // リクエストBodyのgzipをオフに
    },
})
// 作成した Post の ID を出力
fmt.Printf("saved id: %d\n", *result.GetId())

/* 実行結果例
saved id: 4
*/

POSTを使って送信する例はこちら。 こちらも専用のモデルが定義されているのでSetterを利用して値をセットしていく形になります。

試してみたときのはまりどころとしては、デフォルトだとリクエストが圧縮されて送られてしまうことでした。 サーバー側でリクエストが弾かれてしまい、原因を探っていったところBodyをgzipして送信していることが分かりました。 リクエスト時のオプションとしてCompressionをfalseにセットしてあげることで Content-Encoding: gzip に未対応なサーバーでも適切にリクエストを送ることが出来ます。

OpenAPIの生成もコードファーストでやってみる

OpenAPI 3.xではなくOpenAPI 2.0の形式でYAMLを生成していた部分の答え合わせなのですが、今回OpenAPIの生成もGoのコードから作ってみることにしました。

Python だとFastAPIのようなフレームワークで簡単に出力できますが、Goだとフレームワークと一体でOpenAPIを生成してくれるものは見当たりませんでした。 OpenAPIを用意してサーバー側のコードを生成する「スキーマファースト」なやり方であれば、OpenAPI Generatorをはじめとして複数候補がありますが、 OpenAPIを手で書くのが辛い、ファイル分割の対応具合がツールによってまちまちなどの理由で「コードファースト」なやり方を模索してました。

github.com

最終的にたどり着いたのがSwagというツールでした。 こちらはAPIサーバーにコメントの形でOpenAPIに必要な情報を埋め込むことでOpenAPIのスキーマを生成してくれるツールです。 gin, echo, fiberなど主要なWebフレームワークにも対応しています。

type Post struct {
    ID      int    `json:"id" binding:"-"`
    Title   string `json:"title" binding:"required"`
    Content string `json:"content" binding:"required"`
}


// AddPost
// @Summary Create new post
// @Schemes
// @Description add post
// @Tags posts
// @Accept json
// @Produce json
// @Param post body Post true "Post"
// @Success 201 {object} Post "created"
// @Failure 400 {object} ErrResponse "invalid params"
// @Failure 500 {object} ErrResponse "internal server error"
// @Router /posts [post]
func (s *Server) AddPost(g *gin.Context) {
    post := Post{}
    if err := g.ShouldBindJSON(&post); err != nil {
        g.JSON(http.StatusBadRequest, ErrResponse{Detail: err.Error()})
        return
    }
    if err := s.postRepository.AddPost(g, &post); err != nil {
        g.JSON(http.StatusInternalServerError, ErrResponse{Detail: "internal server error"})
        return
    }
    g.JSON(http.StatusCreated, post)
}

このような形で各ハンドラにコメントを付与し、swagコマンドを叩く事でOpenAPIのスキーマを生成できます。 (上記はginの例になります。) Postなどの構造体名をコメントに入れておくとOpenAPIに反映してくれるのでスキーマの更新漏れがなくて便利です。

欠点としては生成されるOpenAPIが2.0相当になってしまう点です。 3.0が公開されたのが2017年なので、OpenAPIまわりのツール対応状況を考えると3.xに移行しておきたいところです。 v2という形でSwagのOpen API 3.x対応が進められていますが現時点ではリリースされていません。 RCまでは来ているようなのでそのうち公開されると思いますが。

まとめ

SwagとKiotaを利用することで、システム間の連携を扱いやすくすることが出来ました。 OpenAPIを利用してスキーマを管理しつつ、プログラムの上ではOpenAPIを意識せず扱えるので改修やメンテナンスも行いやすくなりそうです。

弊社の場合PythonとGoをよく利用するので、「PythonのFastAPIでOpenAPIを生成してKiotaのGoでクライアント生成」、「SwagでOpenAPIを生成してKiotaのPythonクライアントを生成」みたいな言語を跨いでの連携もしやすそうです。 似たようなシステム連携をするのに「Protocol Buffers」も選択肢に上がると思いますが、REST APIでの資産が既にある場合、現状のコードベースを生かしつつSwagなどでOpenAPI化し、自動生成されたクライアントで連携を堅固にしていくのも実用的かと思いました。

今回解説用に作成したサンプルコードはこちら github.com


The Go gopher was designed by Renée French.