Say Hello to Swift OpenAPI Generator

Joel Oliveira
Joel Oliveira
Aug 4 2023
Posted in Engineering & Technology

A Swift Package plugin to help you simplify your codebase

Say Hello to Swift OpenAPI Generator

In this week's post, we will take a look at the Swift OpenAPI Generator, a Swift Package plugin that can help us consume HTTP APIs in iOS apps. If you are not familiar with OpenAPI, it is a widely adopted industry standard. Its conventions and best practices help you work with APIs. For example, this is the standard we use to describe our own REST API.

With OpenAPI, you document the API service in either YAML or JSON. These machine-readable formats allow you to benefit from a rich ecosystem of tooling. There's tools for test generation, runtime validation, interoperability, and in this case, for code generation.

Basically, instead of writing our API request manually using something like URLSession, which requires some code in order to handle our requests and responses, we will use Swift OpenAPI Generator to simplify this process.

For this post, we will use a small API we've written that generates random quotes. For sake of simplicity, it contains one single endpoint and every time it is invoked, it generates a new quote:

Its OpenAPI specification file looks like this:

openapi: "3.0.3"
info:
  title: QuoteGenerator
  version: 1.0.0
servers:
  - url: http://localhost:3000/api
    description: "My quotes API"
paths:
  /quote:
    get:
      operationId: getQuote
      responses:
        '200':
          description: "Returns a random quote"
          content:
            text/plain:
              schema:
                type: string

In this post, we will not explore more complex APIs, instead we'll simply focus on how Swift OpenAPI Generator can simplify our codebase by generating the code needed to consume an API. This Swift Package runs at build time, and the code generated is always in sync with the OpenAPI document and doesn't need to be committed to a source repository.

For this post, we've created a simple app, basically just a single button that, when clicked, will retrieve a quote for us:

But let's start by adding the swift-openapi-generator which provides the Swift Package plugin, the swift-openapi-runtime which provides the common types and abstractions used by the generated code and the swift-openapi-urlsession as our integration package to allow us to use URLSession in our iOS app. You should add these as dependencies in your app's project, under the Package Dependencies tab:

With the dependencies in place, we can configure the target to use the OpenAPI Generator plugin. You can do this in the app's target, by expanding the Run Build Tool Plug-ins section:

We can then add both the openapi.yaml:

openapi: "3.0.3"
info:
  title: QuoteGenerator
  version: 1.0.0
servers:
  - url: http://localhost:3000/api
    description: "My quotes API"
paths:
  /quote:
    get:
      operationId: getQuote
      responses:
        '200':
          description: "Returns a random quote"
          content:
            text/plain:
              schema:
                type: string

And openapi-generator-config.yaml:

generate:
  - types
  - client

To your Xcode project:

The plugin will use these two files to generate all the necessary code.

Let's switch back to ContentView.swift, which will recompile our project so the generated code is ready to use in our app. As a security measure, you'll be asked to trust the plugin the first time you use it.

You can now, add the following code in the ContentView.swift:

import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct ContentView: View {
    @State private var quote = ""

    var body: some View {
        VStack {
            Text(quote).font(.system(size: 30)).multilineTextAlignment(.center)
            Button("Get Quote") {
                Task { try? await updateQuote() }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

    let client: Client

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

    func updateQuote() async throws {
        let response = try await client.getQuote(Operations.getQuote.Input())

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                quote = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("Error: \(statusCode)")
            quote = "Failed to get quote!"
        }
    }
}

Let's go by parts, the view will contain a very basic UI, simply a text (that will use a state property) and a button (that will invoke the HTTP request):

struct ContentView: View {
    @State private var quote = ""

    var body: some View {
        VStack {
            Text(quote).font(.system(size: 30)).multilineTextAlignment(.center)
            Button("Get Quote") {
                Task { try? await updateQuote() }
            }
        }
        .padding()
        .buttonStyle(.borderedProminent)
    }

...more code
}

We will then use the generated code which provides a client property. We will use it in our view with an initializer which configures it to use our API:


struct ContentView: View {
...more code

    let client: Client

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

...more code

And finally, we add the function that makes the API call to the server using that client property:

struct ContentView: View {
...more code

    func updateQuote() async throws {
        let response = try await client.getQuote(Operations.getQuote.Input())

        switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .text(let text):
                quote = text
            }
        case .undocumented(statusCode: let statusCode, _):
            print("Error: \(statusCode)")
            quote = "Failed to get quote!"
        }
    }

...more code
}

You can handle the response, if successful, and retrieve our quote, or in case the server responds with something that isn't specified in its OpenAPI document, you still have a chance to handle that gracefully. As you can see, the generated code substantially simplifies how you use URLSession to consume an API.

And that's basically it, every time we tap the button, it will retrieve a new quote from our API, and display it in the text component:

Cool, right?

Although this is just a simple example, we hope this post helped you understand the benefits of using OpenAPI specification files to help eliminate ambiguity and enable spec-driven development in your iOS apps.

As always, we hope you liked this article, and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news