Swift OpenAPI Generator で様々な生成パターンを確認してみた



Swift OpenAPI Generator は、OpenAPI仕様からSwiftコードを自動生成するツールです。
最近業務で導入してみたのですが、自分の知っている生成方法は一例に過ぎないなと思い、どんな方法があるのか検証してみました。


## 自動生成の方法
まずは生成の方法です。
手動で実行する方法もありますが、Package Plugin を使用した自動生成の方法が推奨されています。

### 1. Package Plugin を使用（推奨）


**前提条件**:
Package.swiftに以下の設定が必要です。

```swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyAPIClient",
    platforms: [.iOS(.v13), .macOS(.v10_15)],
    dependencies: [
        .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "MyAPIClient",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
                .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
            ],
            plugins: [
                .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")
            ]
        ),
    ]
)
```

**必要なファイル**:
プロジェクトルートまたはターゲットディレクトリに以下のファイルを配置：
- `openapi.yaml` - OpenAPI仕様書
- `openapi-generator-config.yaml` - 設定ファイル

**基本的な使用方法**:
```bash
# ビルド時に自動的にコード生成
swift build

# クリーンビルド（生成されたコードも削除）
swift package clean
swift build
```

**生成されるファイルの場所**:
```
.build/plugins/outputs/myapiclient/MyAPIClient/destination/OpenAPIGenerator/GeneratedSources/
├── Types.swift
├── Client.swift
└── Server.swift (設定により生成)
```


### 2. 手動でコード生成
また、手動でもコマンドによって生成することは可能です

```bash
# 基本的な生成コマンド
swift run swift-openapi-generator generate \
  openapi.yaml \
  --config openapi-generator-config.yaml

# 出力ディレクトリを指定
swift run swift-openapi-generator generate \
  openapi.yaml \
  --config openapi-generator-config.yaml \
  --output-directory Sources/Generated
```

## 📑 openapi-generator-config.yaml 完全ガイド

生成される結果は主にopenapi-generator-configに定義されています。各設定がどの様なものか見ていきましょう。

### 設定ファイルの基本構造

```yaml
# 必須設定
generate:              # 生成するコンポーネントを指定
  - types             # 型定義（スキーマ）を生成
  - client            # クライアントコードを生成
  - server            # サーバーコードを生成

# オプション設定
accessModifier: public    # アクセス修飾子（public/internal/package/fileprivate/private）
namingStrategy: defensive # 命名戦略（defensive/optimistic）
filter:                  # フィルタリング設定
  paths: []              # 生成する/除外するパス
  schemas: []            # 生成する/除外するスキーマ

# 追加設定
additionalImports:       # 追加でインポートするモジュール
  - Foundation
  - Combine

featureFlags:           # 実験的機能フラグ
  - ExperimentalObjectOneOf
  - ExperimentalAllOf
  - ExperimentalStringlyTypedRawValues

# オーバーライド設定
nameOverrides:          # 名前のオーバーライド
  schemas:
    OldName: NewName
  operations:
    getOldEndpoint: getNewEndpoint

typeOverrides:          # 型のオーバーライド
  schemas:
    CustomDate: Foundation.Date
```

### 各設定項目の詳細

#### 1. generate（必須）
生成するコンポーネントを指定します。
クライアントで使う場合 `types` `client`だけ生成することが多いかなと思います。

| 値 | 説明 | 生成されるファイル |
|---|---|---|
| `types` | OpenAPIスキーマから型定義を生成 | Types.swift |
| `client` | HTTPクライアントコードを生成 | Client.swift |
| `server` | サーバー実装用プロトコルを生成 | Server.swift |

```yaml
# クライアント開発用
generate:
  - types
  - client

# サーバー開発用
generate:
  - types
  - server

# フルスタック開発用
generate:
  - types
  - client
  - server
```

#### 2. accessModifier（デフォルト: internal）
生成されるコードのアクセスレベルを指定します。
Swift 5.9からはpackageの指定もできるので、より適切な範囲に公開できそうです。

| 値 | 説明 | 使用例 |
|---|---|---|
| `public` | 他のモジュールから参照可能 | ライブラリ開発 |
| `internal` | 同一モジュール内のみ参照可能 | アプリ開発（デフォルト） |
| `package` | 同一パッケージ内で参照可能 | Swift 5.9+ |
| `fileprivate` | 同一ファイル内のみ参照可能 | 特殊用途 |
| `private` | 同一スコープ内のみ参照可能 | 特殊用途 |

```yaml
# ライブラリとして公開する場合
accessModifier: public

# アプリ内部で使用する場合
accessModifier: internal
```

#### 3. namingStrategy（デフォルト: defensive）
名前の衝突の避け方を定義しています。

| 値 | 説明 | 特徴 |
|---|---|---|
| `defensive` | 衝突を避けるため接頭辞/接尾辞を追加 | 安全だが冗長 |
| `optimistic` | 可能な限りシンプルな名前を使用 | 簡潔だが衝突リスクあり |

```yaml
# 安全性重視（推奨）
namingStrategy: defensive

# 簡潔性重視
namingStrategy: optimistic
```

#### 4. filter
特定のパスやスキーマの生成を制御します。
実践ではあまり利用したことがないのですが、巨大なopenapi.yamlだった場合に役に立ちそうです。

```yaml
# 特定のパスのみ生成
filter:
  paths:
    - /users
    - /posts
    - /comments

# 特定のスキーマのみ生成
filter:
  schemas:
    - User
    - Post
    - Comment

# 除外パターン（!で開始）
filter:
  paths:
    - "!*/admin/*"  # adminパスを除外
  schemas:
    - "!Internal*"  # Internal で始まるスキーマを除外
```

#### 5. additionalImports
生成されたコードに追加でインポートするモジュールを指定します。
あまり他の型に依存しない方が扱いやすそうですが、カスタムのimportを定義できる様です。

```yaml
additionalImports:
  - Foundation      # 基本的に必要
  - Combine        # Combine を使用する場合
  - SwiftUI        # SwiftUI との統合
  - MyCustomModule # カスタムモジュール
```

#### 6. featureFlags
実験的機能を有効化します。
基本的にはexperimentalと付いているので利用は慎重に検討する必要があ流と思います。

| フラグ | 説明 |
|---|---|
| `ExperimentalObjectOneOf` | oneOf のサポート（実験的） |
| `ExperimentalAllOf` | allOf のサポート（実験的） |
| `ExperimentalStringlyTypedRawValues` | 文字列型の raw value サポート |

##### ExperimentalObjectOneOf の例
oneOf を使用したユニオン型の生成ができます。

**OpenAPI 仕様**:
```yaml
NotificationPayload:
  oneOf:
    - $ref: '#/components/schemas/EmailNotification'
    - $ref: '#/components/schemas/PushNotification'
```

**生成されるSwiftコード**:
```swift
@frozen public enum NotificationPayload: Codable, Hashable, Sendable {
    case emailNotification(Components.Schemas.EmailNotification)
    case pushNotification(Components.Schemas.PushNotification)
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        
        if let value = try? container.decode(Components.Schemas.EmailNotification.self) {
            self = .emailNotification(value)
            return
        }
        if let value = try? container.decode(Components.Schemas.PushNotification.self) {
            self = .pushNotification(value)
            return
        }
        throw DecodingError.dataCorrupted(/* ... */)
    }
}
```

##### ExperimentalAllOf の例
allOf を使用した継承・合成の生成ができます。

**OpenAPI 仕様**:
```yaml
ExtendedUser:
  allOf:
    - $ref: '#/components/schemas/BaseUser'
    - type: object
      properties:
        lastLoginDate:
          type: string
          format: date-time
```

**生成されるSwiftコード**:
```swift
public struct ExtendedUser: Codable, Hashable, Sendable {
    // BaseUser のプロパティが展開される
    public var id: Swift.Int64
    public var name: Swift.String
    public var email: Swift.String
    
    // 追加のプロパティ
    public var lastLoginDate: Foundation.Date
}
```

##### ExperimentalStringlyTypedRawValues の例
enum の命名を改善できます。

**OpenAPI 仕様**:
```yaml
Priority:
  type: string
  enum:
    - "high-priority"
    - "medium-priority"
    - "low-priority"
```

**フラグ無効時**:
```swift
@frozen public enum Priority: String, Codable {
    case case0 = "high-priority"
    case case1 = "medium-priority"
    case case2 = "low-priority"
}
```

**フラグ有効時**:
```swift
@frozen public enum Priority: String, Codable {
    case highPriority = "high-priority"
    case mediumPriority = "medium-priority" 
    case lowPriority = "low-priority"
}
```

```yaml
featureFlags:
  - ExperimentalObjectOneOf
  - ExperimentalAllOf
  - ExperimentalStringlyTypedRawValues
```

#### 7. nameOverrides
自動生成される名前をオーバーライドします。
スキーマ名称はOpenAPIの都合に則っていることが多いですが、ここでClient側の都合に則ってスキーマを変更することが可能です。

```yaml
nameOverrides:
  schemas:
    # スキーマ名の変更
    user_profile: UserProfile
    post_item: Post
  operations:
    # オペレーション名の変更
    get_user_by_id: getUserById
    list_all_posts: listPosts
```

#### 8. typeOverrides
型のマッピングをカスタマイズします。
OpenAPIで定義されている型がSwiftにあるとは限りません。そのためここでカスタムでClient側の都合の型に変換しています

```yaml
typeOverrides:
  schemas:
    # カスタム日付型を使用
    CustomDate: Foundation.Date
    # カスタムUUID型を使用
    UniqueID: Foundation.UUID
```


## まとめ

Swift OpenAPI Generator による設定の違いをまとめてみました！

完全に自分用ですが、下記でOpenAPIClientGeneratorを実際に動かしてみたので、合わせてご確認いただけると嬉しいです。
https://github.com/entaku0818/swift-test/tree/main/Sources/OpenAPIClientGenerator


## 参考サイト

### 公式ドキュメント
- [Swift OpenAPI Generator - GitHub](https://github.com/apple/swift-openapi-generator)


### 設定リファレンス
- [Generator Configuration Schema](https://github.com/apple/swift-openapi-generator/blob/main/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md)
- [Package Plugin Configuration](https://github.com/apple/swift-openapi-generator/blob/main/Sources/swift-openapi-generator/Documentation.docc/Articles/Using-the-package-plugin.md)


### チュートリアル・サンプル
- [Swift OpenAPI Generator Tutorial](https://github.com/apple/swift-openapi-generator/tree/main/Examples)
- [Getting Started with Swift OpenAPI Generator](https://developer.apple.com/videos/play/wwdc2023/10171/) - WWDC 2023
- [Meet Swift OpenAPI Generator](https://developer.apple.com/documentation/swift_openapi_generator) - Apple Developer Documentation