第2章:OpenAPI設計ガイドと oapi-codegen 実装方法

Go
Golang
Backend
OpenAPI
oapi-codegen
API

はじめに

oapi-codegenは、YAMLでAPIの仕様書を書いたら、あとはGoのコードを自動生成してくれる仕組み。型定義とバリデーションが一発生成されるえらいやつ。 OpenAPIの設計ルールとoapi-codegenの拡張フィールドを理解していく


1. ファイル構成

基本構造

src/openapi/
├── openapi.yml              # 統括ファイル(全定義を集約するルート)
├── paths/                   # エンドポイント定義
│   ├── users/
│   │   ├── index.yml        # GET/POST /users
│   │   └── :userID.yml      # GET/PUT/DELETE /users/{userID}
│   ├── auth.yml             # 認証関連
│   └── health.yml           # ヘルスチェック
└── components/
    └── schemas/             # 再利用可能なスキーマ定義
        ├── users/
        │   ├── index.yml
        │   └── :userID.yml
        ├── common/          # 共通スキーマ(エラーレスポンス等)
        │   └── error_response.yml
        └── enum/            # enum 定義(必ずここに分離)
            └── user_status.yml

構造はURLパスに対応させる。/userspaths/users/index.yml/users/{userID}paths/users/:userID.yml に定義する。共通スキーマは components/schemas/ 配下に置く。enumは必ず components/schemas/enum/ に分ける。

統括ファイル(openapi.yml)でのパス参照

# openapi.yml
openapi: "3.0.3"
info:
  title: My API
  version: "1.0.0"
paths:
  /users:
    $ref: "./paths/users/index.yml"
  /users/{userID}:
    $ref: "./paths/users/:userID.yml"
  /auth/login:
    $ref: "./paths/auth.yml#/login"

$refってなんぞや?↓

  • $refは、YAMLで別ファイルや別セクションを参照するための記法。"./paths/users/index.yml"のようにファイルパスだけ指定すると別ファイル全体を参照し、"./paths/auth.yml#/login"#/login部分は同一ファイル内の特定コンポーネントのみを指す。

2. 命名規則

2.1 ファイル名

lower_snake_case  例: user_profile.yml, error_response.yml

2.2 コンポーネント名(スキーマ名)

UpperCamelCase  例: UserProfile, ErrorResponse, UserIDParameter

2.3 operationId

API 操作の識別子は「動詞 + リソース名」で命名。

動作動詞
一覧取得FetchFetchUsers
単件取得FetchFetchUserByID
作成CreateCreateUser
更新UpdateUpdateUser
削除DeleteDeleteUser
プルダウン用SelectionSelectionPrefectures
# ✅ 良い例
operationId: FetchUsers
operationId: CreateUser
operationId: SelectionPrefectures  # /selections/prefectures の場合

# ❌ 悪い例
operationId: getUsers    # 小文字始まり
operationId: listUsers   # "list" は使わない("Fetch" に統一)

なんで動詞を統一すんの?↓

  • 動詞を統一する理由はAPIの操作を一目で理解できるようにするため。Fetchはデータの取得、Createは新規作成、Updateは更新、Deleteは削除、Selectionは選択肢取得といった具合に、動詞がAPIの目的を明確に伝える。これにより、コードを読んだときに何をするAPIなのかすぐにわかるようになる。

2.4 パラメータ名

in: queryin: path のパラメータ名は camelCase で記載する。

# ✅ 良い例
parameters:
  - name: userID
    in: path
  - name: pageSize
    in: query
  - name: isComplete
    in: query

in ってなんぞや?↓

  • inは、OpenAPIのパラメータ定義で、そのパラメータがどこから来るかを指定するフィールド。in: queryはクエリパラメータ、in: pathはURLパスの一部、in: headerはHTTPヘッダー、in: cookieはクッキーから値を取得することを意味する。

2.5 プロパティ名

JSON プロパティ名は lowerCamelCase を使う。

# ✅ 良い例
properties:
  userName:
    type: string
  projectStatus:
    type: string
  isArchived:
    type: boolean

2.6 スキーマ参照の形式

# paths ファイル内でスキーマを参照する形式
# $ref: {スキーマファイルパス}#{operationId}{Request|Response}

# 例: FetchUserByID の場合
requestBody:
  content:
    application/json:
      schema:
        $ref: "../../components/schemas/users/:userID.yml#/FetchUserByIDRequest"
responses:
  "200":
    content:
      application/json:
        schema:
          $ref: "../../components/schemas/users/:userID.yml#/FetchUserByIDResponse"

3. ディレクトリ構成ルール

URL パス構造をそのままディレクトリ構造に反映する。

/users              → paths/users/index.yml
/users/{userID}     → paths/users/:userID.yml
/projects/{projectID}/tasks  → paths/projects/:projectID/tasks.yml

# 例外(直接 yml ファイルとして配置)
/auth 関連    → paths/auth.yml
/health       → paths/health.yml

enum は必ず専用ディレクトリへ

# ✅ components/schemas/enum/user_status.yml
UserStatus:
  type: string
  enum:
    - active
    - suspended
    - deleted

重要: enum を使用する場合は 必ず components/schemas/enum/ 配下に定義してください。

なんでenumを専用ディレクトリに分けんの?↓

  • enumを専用ディレクトリに分ける理由は、APIの仕様が大規模になるにつれてenum定義が増えるため、管理しやすくするため。components/schemas/enum/にまとめることで、enum定義を一箇所で見つけやすくなり、コードの可読性と保守性が向上するらしい。

4. データ型の定義規則

4.1 日時フィールド(レスポンス)

レスポンスの日時は string 型で定義(date-time 形式は使わん)。

# ✅ 正しい定義
createdAt:
  type: string
  description: "作成日時"
  example: "2024-01-15 10:30:00"

updatedAt:
  type: string
  description: "更新日時"
  example: "2024-01-15 14:45:00"

date-time 形式は使わない理由は、Goのtime.Time型にマッピングされると、RFC3339形式(例: “2024-01-15T10:30:00Z”)ってやつでシリアライズされるため。
これを避けるために、日時フィールドは単純なstring型で定義し、フォーマットはAPIドキュメントの説明やexampleで明示する。

4.2 日時フィールド(リクエスト)

リクエストの日時には Go のカスタム型を指定する。

# ✅ types.CustomTime を使用
startDate:
  type: string
  x-go-type: types.CustomTime
  description: "開始日時"
  example: "2024-01-15 10:30:00"

4.3 Int 型

  • 基本は uint(負の値にならないケース)
  • 負の値が必要な場合は int64

uint知らん↓

  • uintは、符号なし整数を表すGoの基本型。0以上の整数のみを扱う場合に使用される。例えば、ページ番号やアイテム数など、負の値が意味をなさない場合に適している。一方、負の値が必要な場合はint64を使用することが推奨。

  • int64は、符号付き整数を表すGoの基本型。負の値を含む整数を扱う場合に使用される。例えば、残高や温度など、負の値が意味を持つ場合に適している。

4.4 Boolean 型

# ❌ required に boolean を含めない(false が通らなくなる)
required:
  - name
  - isActive  # boolean は required に含めない

# ✅ 正しい定義
required:
  - name
properties:
  isActive:
    type: boolean

4.5 レスポンスは data キーで包む

# ✅ data キーに包む(メッセージレスポンスを除く)
FetchUsersResponse:
  type: object
  required: [data]
  properties:
    data:
      type: array
      items:
        $ref: "#/UserItem"

5. oapi-codegen 拡張フィールド

oapi-codegen には OpenAPI 標準の仕様を拡張するフィールドがある。よく使うものをまとめる。

5.1 x-go-type / x-go-type-import — 外部パッケージ型の使用

components:
  schemas:
    UserProfile:
      type: object
      properties:
        id:
          type: string
          x-go-type: uuid.UUID
          x-go-type-import:
            path: github.com/google/uuid
            name: uuid
        createdAt:
          type: string
          format: date-time
          x-go-type: time.Time
          x-go-type-import:
            path: time
        metadata:
          type: object
          x-go-type: datatypes.JSON
          x-go-type-import:
            path: gorm.io/datatypes

生成される Go コード:

type UserProfile struct {
    ID        uuid.UUID      `json:"id"`
    CreatedAt time.Time      `json:"created_at"`
    Metadata  datatypes.JSON `json:"metadata,omitempty"`
}

全然わからん↓

  • x-go-typeは、OpenAPIの型をGoの特定の型に変換するための指示。例えば、APIドキュメントではidフィールドをstring型で定義していても、実際のGoコードではuuid.UUID型として扱いたい場合に使う。これにより、API仕様と実装コードの両方で適切な型を使用できるようになる。

  • x-go-type-importは、x-go-typeで指定した型が属する外部パッケージをインポートするための情報。pathでパッケージのインポートパスを指定し、nameでコード内で使用するパッケージ名を指定する。これにより、生成されるGoコードに必要なインポート文が自動的に追加される。

5.2 x-oapi-codegen-extra-tags — バリデーション用タグ

components:
  schemas:
    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          x-oapi-codegen-extra-tags:
            binding: "required,email"
        password:
          type: string
          x-oapi-codegen-extra-tags:
            binding: "required,min=8,max=100"
        rememberMe:
          type: boolean
          x-oapi-codegen-extra-tags:
            binding: "omitempty"

生成される Go コード:

type LoginRequest struct {
    Email      string `json:"email" binding:"required,email"`
    Password   string `json:"password" binding:"required,min=8,max=100"`
    RememberMe *bool  `json:"rememberMe,omitempty" binding:"omitempty"`
}

x-oapi-codegen-extra-tagsってなんぞや?↓

  • x-oapi-codegen-extra-tagsは、生成されるGoの構造体フィールドに追加のタグを付与するための拡張フィールド。bindingタグを設定することで、Ginのバリデーションが自動で効くようになる。required,emailmin=8,max=100はGoのバリデーションパッケージのルール書式。

5.3 x-go-name / x-go-json-ignore — カスタムフィールド名と JSON 制御

User:
  type: object
  properties:
    user_id:
      type: string
      x-go-name: "ID" # Go フィールド名を "ID" にカスタマイズ
    internalData:
      type: object
      x-go-json-ignore: true # JSON シリアライズから除外
      properties:
        tempToken:
          type: string

5.4 enum / x-enum-varnames — 列挙型とカスタム変数名

components:
  schemas:
    ErrorCode:
      type: string
      enum:
        - auth_failed
        - invalid_input
        - not_found
        - internal_error
      x-enum-varnames:
        - ErrAuthenticationFailed
        - ErrInvalidInput
        - ErrResourceNotFound
        - ErrInternal

    UserStatus:
      type: string
      enum:
        - active
        - suspended
        - deleted
      x-enum-varnames:
        - StatusActive
        - StatusSuspended
        - StatusDeleted
      x-oapi-codegen-extra-tags:
        binding: "required,oneof=active suspended deleted"

生成される Go コード:

type ErrorCode string

const (
    ErrAuthenticationFailed ErrorCode = "auth_failed"
    ErrInvalidInput         ErrorCode = "invalid_input"
    ErrResourceNotFound     ErrorCode = "not_found"
    ErrInternal             ErrorCode = "internal_error"
)

x-enum-varnamesってなんぞや?↓

  • x-enum-varnamesは、YAMLのenum値に対応するGoの定数名をカスタムで指定できる拡張フィールド。指定しないとYAMLの値(auth_failedなど)がそのまま定数名になってGoらしくないっぽい。ErrAuthenticationFailedのようなGo慣習に沿った名前を付ける。

5.5 deprecated / x-deprecated-reason — 非推奨フィールドの文書化

LegacyUserProfile:
  type: object
  deprecated: true
  x-deprecated-reason: "UserProfile スキーマを使用してください。v3.0 で削除予定です。"
  properties:
    username:
      type: string
      deprecated: true
      x-deprecated-reason: "email フィールドを使用してください"

5.6 x-order — フィールド順序の制御

Address:
  type: object
  properties:
    country:
      type: string
      x-order: 1
    prefecture:
      type: string
      x-order: 2
    city:
      type: string
      x-order: 3
    street:
      type: string
      x-order: 4
    zipCode:
      type: string
      x-order: 5

6. コード生成の設定ファイル

oapi-codegen.yaml の記述例:

package: api
generate:
  models: true
  gin-server: true
  strict-server: true
output-options:
  skip-prune: true
output: generated/openapi/api.gen.go

これは、apiパッケージにモデルとGinサーバーコードを生成し、生成ファイルはgenerated/openapi/api.gen.goに出力する設定。skip-prune: trueは、生成後に未使用のコードを削除しないオプション。

コード生成コマンド:

make codegen

生成されるファイル:

  • generated/openapi/models.gen.go — リクエスト/レスポンスモデル
  • generated/openapi/server.gen.go — サーバーインターフェース(ハンドラーが実装する)

ここはVERSIONというファイルが生成されるように変更されたため、生成されるファイルの説明は削除する。 生成ファイルは手動で編集しないこと。


7. ハンドラー実装時の注意点

クエリパラメータのバリデーション

x-oapi-codegen-extra-tagsbinding タグを設定しても、クエリパラメータのバリデーションは自動では実行されない。ハンドラー内で明示的に BindQuery を呼び出す必要がある。

func (h *productHandler) FetchProducts(c *gin.Context, params api.FetchProductsParams) {
    // ⚠️ バリデーションを必ず呼び出す
    if err := BindQuery(c, &params); err != nil {
        return // BindQuery 内で 400 エラーレスポンスが自動設定される
    }

    // 以降の処理...
    result, err := h.fetchProductsUseCase.Execute(c.Request.Context(), params)
    // ...
}

8. API 設計の原則

エンドポイントのパターン

GET    /resources          一覧取得
GET    /resources/{id}     単件取得
POST   /resources          作成
PUT    /resources/{id}     全体更新
PATCH  /resources/{id}     部分更新
DELETE /resources/{id}     削除
GET    /selections/{name}  プルダウン用一覧

ステータスコード

操作コード
取得成功200 OK
作成成功201 Created
削除成功(レスポンスあり)200 OK
バリデーションエラー400 Bad Request
未認証401 Unauthorized
権限なし403 Forbidden
見つからない404 Not Found
サーバーエラー500 Internal Server Error

注意: 422 Unprocessable Entity は使わず、バリデーションエラーは 400 に統一する。


まとめ

ポイント内容
ファイル命名lower_snake_case
スキーマ名UpperCamelCase
operationIdFetch / Create / Update / Delete + リソース名
enum必ず components/schemas/enum/ に分離
日時(レスポンス)string 型、"2024-01-15 10:30:00" 形式
日時(リクエスト)x-go-type: types.CustomTime
バリデーションx-oapi-codegen-extra-tags + BindQuery() の明示呼び出し
生成ファイル手動編集禁止
← Return to blog
↑ Back to top