Swift
Generate a native Swift client from your routes with the ts-kizuna-swift CLI.
@ts-kizuna/swift generates a native Swift client from your ts-kizuna routes. The generated client uses URLSession and Codable, with no third-party Swift dependencies required.
pnpm add @ts-kizuna/swiftbun add @ts-kizuna/swiftnpm install @ts-kizuna/swiftGenerate
ts-kizuna-swift generate --contract src/contract.ts --output ios/MyApp/Generated/APIClient.swift --namespace-name API| Flag | Description |
|---|---|
--contract | Path to the TypeScript file default-exporting your k.contract |
--output | Output path for the generated .swift file |
--namespace-name | Public enum wrapping all generated types (e.g. API) |
--camel-case | Convert wire field names to camelCase properties with CodingKeys |
--unknown-enum-case | Emit enums with an unknown(String) fallback so new server values don't break decoding |
The CLI reads the contract at runtime using jiti, with no build step needed. Add the generated file to your Xcode project or Swift package as a regular source file.
Property naming
By default, field names are emitted verbatim, so total_count on the wire stays total_count in the generated type. The types mirror the wire shape, so it's easy to see what came from the API.
Pass --camel-case to convert wire fields to camelCase properties (total_count becomes totalCount), preserving the wire name via CodingKeys.
Unknown enum case (open enums)
By default a z.enum is a closed Swift enum that throws on a wire value it doesn't know, failing the whole response. Pass --unknown-enum-case to make enums forward-compatible: each gains an unknown(String) case, so an unrecognised value decodes to .unknown("newValue") instead of throwing:
switch event.kind {
case .login, .logout, .signup:
render(event)
case .unknown(let raw):
log("skipping unknown event kind: \(raw)")
}The raw value is preserved, so re-encoding writes it back unchanged. Discriminated unions still throw on an unknown discriminator.
Usage
Create the client once and share it across your app:
let client = APIClient(baseURL: URL(string: "https://api.example.com")!)Request inputs are passed as components named after the contract's input groups: .params (path), .body, .query, and .headers:
// object body
let created = try await client.users.createUser(
.body(
name: "Ada",
email: "ada@example.com"
)
)
// path param + header
let user = try await client.users.getUser(
.params(id: "42"),
.headers(xRequestId: "trace-1")
)
// query
let page = try await client.users.listUsers(
.query(page: 1, limit: 20)
)
let all = try await client.users.listUsers()
// discriminated-union body
try await client.sendNotification(
.body(
.email(to: "ada@example.com", subject: "Hi")
)
)Generated client
The generated file exports a Sendable client with methods for each route and sub-clients for grouped routes. It's a plain final class, and everything is immutable let storage set at init, so it's safe to share across tasks without an actor hop per request. Each request group is a nested struct with a group-named factory:
import Foundation
public final class APIClient: Sendable {
public let baseURL: URL
public let session: URLSession
public var users: APIUsersClient { ... }
public func listUsers(_ query: APIClient.ListUsers.Query = .query()) async throws(APIClient.ListUsers.Failure) -> APIClient.ListUsers.Result {
// ...
}
public enum ListUsers {
public struct Query: Sendable {
public let page: Int?
public let limit: Int?
public init(page: Int? = nil, limit: Int? = nil) { ... }
public static func query(page: Int? = nil, limit: Int? = nil) -> Self { .init(page: page, limit: limit) }
}
// Response, Result, Failure ...
}
}Deprecation
Routes and fields marked /** @deprecated */ in the routes emit @available(*, deprecated) in the generated Swift code:
@available(*, deprecated)
public func deleteUser(_ params: APIClient.DeleteUser.Params) async throws(APIClient.DeleteUser.Failure) {
// ...
}Tip: automate with a script
You can add a script to your package.json to regenerate the client whenever the contract changes:
{
"scripts": {
"generate:swift": "ts-kizuna-swift generate --contract src/contract.ts --output ../ios/MyApp/Generated/APIClient.swift --namespace-name API"
}
}