WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift OpenAPI Generator

Meet Swift OpenAPI Generator

Watch original video

Highlight

Swift OpenAPI Generator is a Swift Package plug-in that automatically generates type-safe client and server code based on OpenAPI documentation at build time, eliminating handwritten network request boilerplate code and ensuring that code is always in sync with API documentation.

Core Content

There are many things to consider when calling a server-side API: What is the base URL? How to spell the path? Should I use GET or POST for HTTP method? Should the parameter be put in query or body? What is the data structure returned?

Handwritten documents are easily outdated, and reading the source code is time-consuming and laborious. OpenAPI describes APIs in machine-readable YAML or JSON, becoming a single source of truth between servers and clients.

01:58

Pain points of handwritten API calls

The simplest API call requires so many steps in handwritten code:

// 1. Parse the base URL
var components = URLComponents(string: "http://localhost:8080/api")!
// 2. Add the path
components.path = "/greet"
// 3. Add query parameters
components.queryItems = [URLQueryItem(name: "name", value: "World")]
// 4. Construct the request
let request = URLRequest(url: components.url!)
// 5. Start the request
let (data, response) = try await URLSession.shared.data(for: request)
// 6. Validate the response
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw APIError.invalidResponse
}
// 7. Decode JSON
struct Greeting: Decodable {
    let message: String
}
let greeting = try JSONDecoder().decode(Greeting.self, from: data)
// 8. Use the result
print(greeting.message)

This is just a simple GET request. Real-world APIs have hundreds of operations, various request headers, authentication, error codes, and content negotiation. Writing this code by hand is repetitive, verbose, and error-prone.

02:55

OpenAPI Documentation

The OpenAPI documentation describes the complete contract of the API:

openapi: "3.0.3"
info:
  title: "GreetingService"
  version: "1.0.0"
servers:
- url: "http://localhost:8080/api"
  description: "Production"
paths:
  /greet:
    get:
      operationId: "getGreeting"
      parameters:
      - name: "name"
        required: false
        in: "query"
        description: "Personalizes the greeting."
        schema:
          type: "string"
      responses:
        "200":
          description: "Returns a greeting"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Greeting"

The document includes: OpenAPI version, API metadata, server address, path and operation, parameter definition, response definition. A document is the complete specification of an API.

04:17

Generated client code

The Swift OpenAPI Generator reads OpenAPI documentation at build time and automatically generates type-safe Swift code. Use the generated client:

import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

struct ContentView: View {
    @State private var emoji = "🫥"

    var body: some View {
        VStack {
            Text(emoji).font(.system(size: 100))
            Button("Get cat!") {
                Task { try? await updateEmoji() }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: Client

    init() {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji() async throws {
        let response = try await client.getEmoji(
            Operations.getEmoji.Input()
        )

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                emoji = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("cat-astrophe: \(statusCode)")
            emoji = "🙉"
        }
    }
}

Key points:

  • ClientIs a generated type that contains methods for all API operations -Servers.server1()Generated from the server definition of the OpenAPI documentation -Operations.getEmoji.Input()is a type-safe request input
  • The response is of enumeration type,.okand.undocumentedOverride defined and undefined responses in the document
  • The compiler forces you to handle every response situation

08:03

Detailed Content

Project configuration

existPackage.swiftAdd dependencies in:

// swift-tools-version: 5.8
import PackageDescription

let package = Package(
    name: "CatService",
    platforms: [.macOS(.v13)],
    dependencies: [
        .package(
            url: "https://github.com/apple/swift-openapi-generator",
            .upToNextMinor(from: "0.1.0")
        ),
        .package(
            url: "https://github.com/apple/swift-openapi-runtime",
            .upToNextMinor(from: "0.1.0")
        ),
        .package(
            url: "https://github.com/apple/swift-openapi-urlsession",
            .upToNextMinor(from: "0.1.0")
        ),
    ],
    targets: [
        .executableTarget(
            name: "CatService",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
                .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
            ],
            plugins: [
                .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
            ]
        ),
    ]
)

Key points:

  • swift-openapi-generatoris a build plugin that generates code at compile time -swift-openapi-runtimeProvide runtime types and protocols -swift-openapi-urlsessionIs the URLSession transport layer implementation
  • Plugin inpluginsDeclared in, it will run automatically when building

18:43

Generate configuration

createopenapi-generator-config.yamlControlling generated content:

generate:
  - types
  - client

typesGenerate data types (request input, response output, model structure).clientGenerate client code. If you want to implement the server side, replace it withserver

09:48

API call with parameters

Parameters defined in the OpenAPI documentation automatically generate type-safe input types:

func updateEmoji(count: Int = 1) async throws {
    let response = try await client.getEmoji(
        Operations.getEmoji.Input(
            query: Operations.getEmoji.Input.Query(count: count)
        )
    )

    switch response {
    case let .ok(okResponse):
        switch okResponse.body {
        case .text(let text):
            emoji = text
        }
    case .undocumented(statusCode: let statusCode, _):
        print("cat-astrophe: \(statusCode)")
        emoji = "🙉"
    }
}

Operations.getEmoji.Input.Query(count: count)Is a type-safe query parameter constructor. If the OpenAPI documentation putscountdefined asinteger, pass hereStringWill compile and report an error.

13:24

Mock Client for testing

generatedAPIProtocolProtocols make mocking easy:

struct ContentView<C: APIProtocol>: View {
    @State private var emoji = "🫥"
    let client: C

    init(client: C) {
        self.client = client
    }

    init() where C == Client {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }

    func updateEmoji(count: Int = 1) async throws {
        let response = try await client.getEmoji(
            Operations.getEmoji.Input(
                query: Operations.getEmoji.Input.Query(count: count)
            )
        )
        // ...
    }
}

struct MockClient: APIProtocol {
    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        let count = input.query.count ?? 1
        let emojis = String(repeating: "🤖", count: count)
        return .ok(Operations.getEmoji.Output.Ok(
            body: .text(emojis)
        ))
    }
}

ContentViewUse generic parametersC: APIProtocol, for production environmentClient, for test environmentMockClient. SwiftUI preview can also be injected directlyMockClient

15:06

Server-side implementation

The same OpenAPI documentation can also generate server-side code. accomplishAPIProtocolThat’s it:

import Foundation
import OpenAPIRuntime
import OpenAPIVapor
import Vapor

struct Handler: APIProtocol {
    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        let candidates = "🐱😹😻🙀😿😽😸😺😾😼"
        let chosen = String(candidates.randomElement()!)
        let count = input.query.count ?? 1
        let emojis = String(repeating: chosen, count: count)
        return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis)))
    }
}

@main
struct CatService {
    public static func main() throws {
        let app = Vapor.Application()
        let transport = VaporTransport(routesBuilder: app)
        let handler = Handler()
        try handler.registerHandlers(on: transport, serverURL: Servers.server1())
        try app.run()
    }
}

Key points:

  • HandlerfollowAPIProtocol, realize all operations -input.query.countis type-safe parameter access
  • return.ok(...)Constructing a type-safe response -registerHandlers(on:serverURL:)Automatically register all routes

Server configurationgenerateChange toserver

generate:
  - types
  - server

16:58

API Evolution

When new operations are added to the API, simply update the OpenAPI documentation:

paths:
  /emoji:
    get:
      operationId: getEmoji
      # ...
  /clip:
    get:
      operationId: getClip
      responses:
        '200':
          description: "Returns a cat video!"
          content:
            video/mp4:
              schema:
                type: string
                format: binary

After rebuilding, the generated code automatically includes the new operations. Server-side implementation needs to add corresponding methods:

struct Handler: APIProtocol {
    func getClip(_ input: Operations.getClip.Input) async throws -> Operations.getClip.Output {
        let clipResourceURL = Bundle.module.url(forResource: "cat", withExtension: "mp4")!
        let clipData = try Data(contentsOf: clipResourceURL)
        return .ok(Operations.getClip.Output.Ok(body: .binary(clipData)))
    }

    func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output {
        // ...
    }
}

The compiler will force you to implement all the operations defined in the document, and nothing will be missed.

20:11

Core Takeaways

Migrate existing handwritten network layer to OpenAPI generation

If your project has a REST API call layer, convert the interface document into OpenAPI YAML and replace handwritten code with Swift OpenAPI Generator. One migration brings long-term benefits - when the API changes, you only need to change the documentation and rebuild it.

Entry: Export OpenAPI documentation from an existing API documentation or Postman collection, createopenapi-generator-config.yamlConfigure generated client code.

Establish a unified API contract for internal microservices

When multiple services within the team call each other, OpenAPI documents are used as contracts. The server and client generate code from the same document to ensure that both parties have a consistent understanding of the API.

Entry: maintained in Git repositoryapi.yaml, CI verifies document legality and generates code when building.

Use MockClient for UI testing and preview

SwiftUI previews and unit tests often need to simulate network responses. useMockClientaccomplishAPIProtocol, returns fixed data so that preview and testing do not depend on the real network.

Entry: Create a correspondingMockClientvariant, in#Previewand injected into test cases.

Quickly build backend with Vapor + OpenAPI

Teams with experience in iOS development can write the backend in Swift. Vapor + Swift OpenAPI Generator allows you to focus on business logic, and route registration, parameter parsing, and response construction are all automatically generated.

Entry: Create Vapor project, addswift-openapi-vapordependency, implementationAPIProtocolThat’s it.

Comments

GitHub Issues · utterances