Skip to main content

Swift and Convex type conversion

Custom data types

Convex lets you easily express your data in the backend as TypeScript objects, and can return those objects from queries, mutations and actions. To handle objects on the Swift side, create struct definitions that conform to the Decodable protocol. Usually that’s fairly trivial to do, as any struct with all Decodable members can automatically conform.

Consider a Convex query function that returns results like this JavaScript object:

{
name: "Guardians",
uniformColors: ["blue", "white", "red"],
wins: 80n,
losses: 60n
}

That can be represented in Swift using:

struct BaseballTeam: Decodable {
let name: String
let uniformColors: [String]
@ConvexInt
var wins: Int
@ConvexInt
var losses: Int
}

Then you can pass that type as the yielding argument in your subscribe call:

convex.subscribe(to: "mlb:first_place_team",
with: ["division": "AL Central"],
yielding: BaseballTeam.self)

The data from the remote function will be deserialized to your custom struct. Often your use of the type can be inferred from the calling context, and you can skip the yielding argument.

Numerical types

Numeric types like Int and Double are encoded in a special format to ensure proper interoperation with your TypeScript backend functions. To safely use them on the Swift side, ensure that you use one of the following property wrappers.

TypeWrapper
Float or Double@ConvexFloat
Float? or Double?@OptionalConvexFloat
Int or Int32 or Int64@ConvexInt
Int? or Int32? or Int64?@OptionalConvexInt

Note that struct properties with wrappers must be declared as var.

Field name conversion

If your code receives objects with names that you need to or want to translate to different names, you can use a CodingKeys enum to specify a mapping of remote names to names on your struct. For example, imagine a backend function or API that returns log entries like the following representing when someone came in and went out:

{name: "Bob", in: "2024-10-03 08:00:00", out: "2024-10-03 11:00:00"}

That data can’t decode directly into a struct because in is a keyword in Swift. We can use CodingKeys to give it an alternate name while still ingesting the data from the original name.

struct Log: Decodable {
let name: String
let inTime: String
let outTime: String

enum CodingKeys: String, CodingKey {
case name
case inTime = "in"
case outTime = "out"
}
}

Putting it all together

In the custom data type example above, JavaScript's BigInt type is used in the backend data by adding a trailing n to the wins and losses values which lets the Swift code use Int. If instead the code used regular JavaScript number types, on the Swift side those would be received as floating point values and deserialization to Int would fail.

If you have a situation like that where number is used but by convention it only contains integer values, you can handle that in your struct by using field name conversion and custom properties to hide the floating point representation.

struct BaseballTeam: Decodable {
let name: String
let uniformColors: [String]
@ConvexFloat
private var internalWins: Double
@ConvexFloat
private var internalLosses: Double

enum CodingKeys: String, CodingKey {
case name
case uniformColors
case internalWins = "wins"
case internalLosses = "losses"
}

// Expose the Double values as Ints
var wins: Int { Int(internalWins) }
var losses: Int { Int(internalLosses) }
}

The pattern is to store the Double values privately and with different names than the value from the backend. Then add custom properties to provide the Int values.