Swift
The Swift SDK is an idiomatic package emitted from the same GlottoIR as every other target.
Operations under a declared resource hang off an accessor — client.pets.listPets() — while an
operation you leave ungrouped stays a method on Client. Request/response shapes are Codable
Swift types, so you work with real types rather than Data.
Quickstart
// In your Package.swift, add the generated package to `dependencies`
// (a local `.package(path:)` or your own published URL), then its library product:
.product(name: "Petstore", package: "Petstore")
import Foundation
import Petstore
let client = Client(
baseURL: "https://api.petstore.example",
token: ProcessInfo.processInfo.environment["PETSTORE_TOKEN"]!
)
// a single call
let pet = try await client.pets.createPet(body: NewPet(name: "Rex"))
// For an operation configured with pagination:
let pets = client.pets.listPetsScoped()
defer { pets.close() }
for try await pet in pets {
print(pet.name)
}
Typed models
Each GlottoIR model becomes a Codable struct; operations decode and return the typed response
and accept a typed request body. Discriminated unions resolve to the right enum case.
Typed errors
Operations throw an APIError struct carrying the parsed error body, with an APIErrorKind
enum so callers switch error.kind / if case .notFound rather than matching status codes. See
Errors.
Pagination
Scoped companions return a StreamScope that fetches another page only when you advance beyond
the current page. Put close() in a defer block to release it when your code breaks, returns, or
throws. Existing AsyncThrowingStream methods remain available; cancel their consuming task when
leaving early. See Pagination.
Manual Page companions expose typed items and explicit nextPage() navigation. The throwing response() accessor decodes the full wrapper without another request. See manual pagination.
Retries & backoff
Pass RequestOptions to override headers, timeouts, retry counts and idempotency for an individual operation. Method defaults apply before caller overrides. See request options and defaults.
Transient failures (5xx, 429, transport errors) retry with exponential backoff and jitter,
configurable per client. See Retries & timeouts.
SSE, NDJSON, and WebSocket streaming
SSE and NDJSON Scoped companions provide the same StreamScope ownership pattern as pagination.
The opening deadline and retry policy apply until the first response byte. After that boundary,
streams outlive the opening timeout and read failures surface without replay.
WebSocket operations retain their typed connection API. Put connection.close() in defer when
leaving a message loop early. The upgrade uses the configured client, and a successful 101 ends
the opening deadline before the first application frame arrives. See Streaming
and Authentication.
Toolchain and Linux requirements
Every generated Swift package requires Swift 6.1 or newer (Xcode 16.3 or newer on Apple platforms).
SDKs with binary downloads, WebSocket operations, GraphQL subscriptions or event channels also need
the system zlib development library on Linux (zlib1g-dev on Debian/Ubuntu). Their Linux transport
uses SwiftNIO and NIOSSL; Apple defaults use Foundation.
For Client binary-download and WebSocket methods, supplying session: on Linux also requires an
explicit streamingTransport:. Choose NIOStreamingTransport() or implement StreamingTransport
for custom policy. The built-in transport verifies TLS certificates and hostnames and accepts
optional PEM trust roots and a client certificate/key; it does not follow redirects or supply
cookie storage, caching, proxies or authentication-challenge handling. A supplied URLSession
continues to serve ordinary requests, and its policy is not copied into the streaming transport.
GraphQLTransport and EventTransport remain the custom-transport interfaces for their respective
clients. The generated README includes the applicable configuration example.
Unknown response fields
Your API can add a response field without it being a breaking change — but a generated Codable struct decodes only its declared CodingKeys, so the key would be dropped. The generated
models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable
through an accessor, and written back out when the model is re-serialized.
let pet = try await client.pets.createPet(body: body)
// A field your API started returning after this SDK was generated.
if let species = pet.extraFields["species"] {
print(species)
}
// Re-encoding preserves it — a read-modify-write never silently drops it.
let json = try JSONEncoder().encode(pet)
extraFields is a let holding [String: JSONValue], so nested objects and arrays survive
intact, and retention is recursive.
Your existing construction still compiles: the memberwise initializer keeps its original signature and starts the model with an empty bag.
The retained fields are read-only by design. To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than writing to the retained bag.
Missing and null fields
By default an optional property that decodes from JSON null is nil, the same as an omitted
key, and encodeIfPresent leaves it out on re-encode — so a read-modify-write sends a decoded
null back as an absent key. Set
targets.swift.model_presence: explicit to keep missing,
null and a value apart: declared properties become Field<Value> (.missing, .null,
.value(_)). That changes property and constructor argument types; the generated README shows
the idioms.
Binary downloads
Binary endpoints return an owned BinaryResponse with response metadata. readAll(maxBytes:) bounds delivered bytes; copy(to:) accepts a FileHandle or async sink closure. Use defer { download.close() } after opening an incremental reader. Mixed JSON/binary operations expose an explicit result branch. See file transfers.
File uploads
multipart/form-data operations build the multipart body from their fields for you; an
application/octet-stream operation takes a positional body: Data argument sent raw with the
right Content-Type (Data? when the body is optional, see below).
Optional request bodies
When your spec marks a request body optional (required: false), the argument is a defaulted
optional: body: Model? = nil for a typed body, Bool? (or Int?, String?) for a scalar one,
AnyCodable? for an inline object and Data? for an octet-stream one. Leave it off to send no body
and no Content-Type, or pass a value to send it. A falsy value is still a value:
false, 0, "" and AnyCodable(nil) are sent as written. A body whose schema admits null (marked nullable on the $ref or on the schema it names, or a union
with a null member) is Model??: the outer nil leaves it out and .some(nil) sends JSON null. An optional
multipart/form-data body makes every part optional: the parts you pass are the parts sent, and
passing none sends no body. The same holds on paginated operations, where the body is re-sent
unchanged on every later page. An operation whose config sets
auto_populate body_fields is the one exception
to “no body”: it sends just those constants (with their Content-Type) when you leave the body out.
A required body stays a plain argument. On a paginated operation (the iterator, its Scoped companion
and its Page companion) a required body is the body: argument, and the call without it that
compiled before these methods sent bodies is kept as an overload that sends no body of yours, so
existing client.items.list() call sites keep compiling; a body whose schema admits null is
Model? there, where nil sends JSON null and the overload without body: leaves it out.
Keychain secure storage
Opt-in. The Keychain target is compile-verified against a real Apple toolchain (macOS) by the scheduled
mobile-compileCI lane — the#if canImport(Security)Keychain store builds on a real Apple SDK. The Swift SDK above is unaffected.
Opt in with targets.swift.keychain: true in your glotto.yml to add a pluggable secure token store
to the bearer client — the same native-mobile secure-storage seam the React Native SDK ships:
targets:
swift:
keychain: true
The client gains an optional tokenStore: SecureTokenStore? parameter; when supplied, the bearer
token is read from it per request (falling back to the static token). A Keychain-backed
implementation (KeychainTokenStore, over the Security framework SecItem generic-password API) is
generated for you. SecureTokenStore is a plain protocol, so you can supply your own store too. The
Keychain implementation is #if canImport(Security)-guarded, so the SDK still compiles on Linux
(where the Security framework is absent) — exactly like the engine’s streaming Linux fallbacks.
Biometric-gated storage
Opt-in, experimental.
targets.swift.keychainBiometric: truerequirestargets.swift.keychain: trueand is compile-verified on the scheduledmobile-compilelane. The plain Keychain store above stays the default and byte-identical.
For credentials that must be released only after the user authenticates, add
targets.swift.keychainBiometric: true:
targets:
swift:
keychain: true
keychainBiometric: true
This additionally emits a BiometricKeychainTokenStore (alongside the plain KeychainTokenStore) whose
Keychain item carries a SecAccessControlCreateWithFlags(.biometryCurrentSet) access control, so the OS
presents the biometric prompt on access. An optional LAContext (LocalAuthentication-guarded) lets you
reuse an existing authentication or customize the prompt. It rides the same #if canImport(Security)
guard, so the SDK still compiles on Linux.
Connectivity-aware retries & lifecycle-aware backoff
Opt-in. The concrete monitors are compile-verified against a real Apple toolchain (macOS) by the scheduled
mobile-compileCI lane — the#if canImport(Network)/#if canImport(UIKit)monitors build on a real Apple SDK. The Swift SDK above is unaffected.
The retry loop can be made network- and lifecycle-aware — the two remaining native-mobile seams the React Native and Kotlin-Android SDKs ship. They are two independent opt-in flags:
targets:
swift:
connectivity: true # pause retries while the device is offline
lifecycle: true # pause backoff while the app is backgrounded
With connectivity on, the Client gains an optional connectivity: ConnectivityMonitor? parameter.
Before each (re)try, a known-offline state is treated as transient — the client waits and re-checks
rather than burning the request. A concrete NWPathMonitorConnectivity (over the Network framework)
is generated for you.
With lifecycle on, the Client gains an optional lifecycle: AppLifecycle? parameter. While the app
is backgrounded (where the OS freezes timers), the client pauses the backoff without consuming a
retry attempt until the app returns to the foreground. A concrete UIApplicationLifecycle (over
UIKit lifecycle notifications) is generated for you.
ConnectivityMonitor and AppLifecycle are plain protocols, so you can supply your own implementations
too. Both concrete monitors are #if canImport(...)-guarded (Network / UIKit), so the SDK still
compiles on Linux where those frameworks are absent — exactly like the Keychain store above.