Custom Scalars
In addition to its built-in scalar types (Int
, String
, etc.), GraphQL supports defining custom scalars. For example, your schema might provide a custom scalar for Date
, UUID
, or GeoLocation
.
Custom Scalars are initially defined as part of a schema. To interact with a schema using custom scalars, your client must define a Swift type to use for each custom scalar.
Apollo iOS automatically defines Swift types for all of the built-in scalar types:
GraphQL Type | Swift Type |
---|---|
Int | Int |
Float | Double |
Boolean | Bool |
String | String |
ID | String |
By default, each custom scalar is treated as a Swift String
, but you can customize the type of all of your custom scalars with Apollo iOS!
Generating custom scalar types
If any part of your GraphQL application references a custom scalar defined by the schema, a file for it will be generated in your generated schema output. This file can be used to define the Swift type for the custom scalar.
For example, in a schema that defines a custom scalar:
1 scalar UUID
Apollo iOS generates a UUID
custom scalar type in your generated schema output. This generated file defines UUID
as a String
by default.
1public extension MySchema {
2 typealias UUID = String
3}
The MySchema.UUID
type will be referenced in any other generated objects that reference the UUID
scalar.
Because custom scalar files are only generated once, they can then be edited, and your changes will never be overwritten by subsequent code generation execution.
You can edit this file to define a different type for the UUID
scalar type.
Note: Custom scalars may be referenced by fields on GraphQL types, values for input parameters, or on input objects. Because custom scalars are only generated when they are referenced by your operations, they may be added to your project during any future execution of code generation, not just on the initial execution.
Defining a custom scalar type
You can define the type for a custom scalar by creating a new type, or by pointing the typealias to another existing type.
The type for your custom scalar must conform to the CustomScalarType
protocol. This requires you to implement the JSON serialization functionality for your custom scalar type.
To implement the CustomScalarType
protocol:
1. Implement the _jsonValue
property.
This converts the scalar value into a JSONValue
that can be serialized into a JSON dictionary. The value of the _jsonValue
property will be stored in the cache as the JSON value for the custom scalar.
Usually, this should be identical to the value received from the network response to represent the custom scalar.
2. Implement the init(_jsonValue:)
initializer.
This initializer is used to construct the custom scalar value from a JSONValue
. When constructing the scalar from a network response, the value of the _jsonValue
parameter will be the value from the network response. When constructing the scalar from a cached value, this will be the value provided in the _jsonValue
property.
If the value provided is unrecognized, you should throw an error from this function. Apollo iOS provides JSONDecodingError
in the ApolloAPI
library, but you may throw any custom error you wish.
3. Implement the Hashable
and Equatable
protocols if needed.
If your custom scalar type does not already conform to Hashable
and Equatable
, you will need to implement hash(into:)
and the ==
operator to conform to each of these protocols respectively.
Example: UUID
For example, you could point the typealias
for UUID
to the Foundation.UUID
type:
1import Foundation
2
3public extension MySchema {
4 typealias UUID = Foundation.UUID
5}
6
7extension Foundation.UUID: CustomScalarType {
8 public init (_jsonValue value: JSONValue) throws {
9 guard let uuidString = value as? String,
10 let uuid = UUID(uuidString: uuidString) else {
11 throw JSONDecodingError.couldNotConvert(value: value, to: Foundation.UUID.self)
12 }
13
14 self = uuid
15 }
16
17 public var _jsonValue: JSONValue {
18 uuidString
19 }
20}
Example: GeoPoint
Alternatively, you could create your own custom scalar type. In this case, replace the typealias
with the new type.
For example, a custom scalar GeoPoint
that has a JSON representation of "100.0,10.0"
could be implemented like this:
1import Foundation
2
3extension MySchema {
4 public struct GeoPoint: CustomScalarType, Hashable {
5 let x: Float
6 let y: Float
7
8 public init (_jsonValue value: JSONValue) throws {
9 let coordinates = try (value as? String)?
10 .components(separatedBy: ",")
11 .map { try Float(_jsonValue: $0) }
12
13 guard let coordinates, coordinates.count == 2 else {
14 throw JSONDecodingError.couldNotConvert(value: value, to: GeoPoint.self)
15 }
16
17 self.x = coordinates[0]
18 self.y = coordinates[1]
19 }
20
21 public var _jsonValue: JSONValue {
22 "\(String(format: "%.1f", x)),\(String(format: "%.1f", y))"
23 }
24 }
25}
The GeoPoint
struct conforms to both CustomScalarType
and Hashable
. You must explicitly declare the conformance to Hashable
, which inherits Equatable
conformance. Because Swift can synthesize the Hashable
and Equatable
conformancs here, you do not need to implement them.
The init(_jsonValue:)
initializer casts the JSONValue
as a String
and separates it into two coordinates. It converts those coordinates using Float(_jsonValue:)
which is provided by Apollo. Each of the built-in scalar types has JSON serialization support that you can use within your custom scalar implementations.
To ensure consistency of the serialized JSON, the _jsonValue
function ensures that the coordinates are formatted with a single decimal point using String(format:,_:)
.
JSON and other custom scalars with multiple return types
Some custom scalars are set up to potentially return multiple types at runtime. This is not ideal since you lose type safety, but if you're using an API you don't have control over, there's often not a great alternative to this.
When this happens, because you don't know the type that's coming in, you can't set up a single typealias
for that scalar. Instead, you need to define some other way of instantiating your custom scalar object.
This happens most often with JSON, which can return either an array or a dictionary. Here's an example of how you can use an enum to allow dynamic-but-limited types to parse (with CustomJSON
as a placeholder type name`):
1extension MySchema {
2 public enum CustomJSON: CustomScalarType, Hashable {
3 case dictionary([String: AnyHashable])
4 case array([AnyHashable])
5
6 public init(_jsonValue value: JSONValue) throws {
7 if let dict = value as? [String: AnyHashable] {
8 self = .dictionary(dict)
9 } else if let array = value as? [AnyHashable] {
10 self = .array(array)
11 } else {
12 throw JSONDecodingError.couldNotConvert(value: value, to: CustomJSON.self)
13 }
14 }
15
16 public var _jsonValue: JSONValue {
17 switch self {
18 case let .dictionary(json as AnyHashable),
19 let .array(json as AnyHashable):
20 return json
21 }
22 }
23
24 public static func == (lhs: CustomJSON, rhs: CustomJSON) -> Bool {
25 lhs._jsonValue == rhs._jsonValue
26 }
27
28 public func hash(into hasher: inout Hasher) {
29 hasher.combine(_jsonValue)
30 }
31 }
32}