WARNING: This package is in beta. It's API is still evolving and is subject to breaking changes.
This is a Swift package plugin that generates server-side GraphQL API code from GraphQL schema files, inspired by GraphQL Tools' makeExecutableSchema and Swift's OpenAPI Generator.
- Data-driven: Guarantee conformance with the declared GraphQL spec
- Build-time code generation: Code is generated at build time and doesn't need to be committed
- Type-safe: Leverages Swift's type system for compile-time safety
- Minimal boilerplate: Generates all GraphQL definition code - you write the business logic
- Flexible: Makes no assumptions about backing data types other than GraphQL type conformance
Add the package to your Package.swift. Be sure to add the GraphQLGeneratorRuntime dependency to your package, and add the GraphQLGeneratorPlugin to the plugins section:
dependencies: [
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.1.0"),
.package(url: "https://github.com/GraphQLSwift/graphql-generator", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "GraphQL", package: "GraphQL"),
.product(name: "GraphQLGeneratorRuntime", package: "graphql-generator"),
],
plugins: [
.plugin(name: "GraphQLGeneratorPlugin", package: "graphql-generator")
]
)
]Protip: Take a look at the projects in the Examples directory to see real, fully featured examples.
Create a .graphql file in your target's Sources directory:
Sources/YourTarget/schema.graphql:
type User {
name: String!
email: EmailAddress!
}
type Query {
user: User
}When you build, the plugin will automatically generate Swift code. If you want, you can view it in the .build/plugins/outputs directory:
BuildGraphQLSchema.swift- DefinesbuildGraphQLSchemafunction that builds an executable schema.GraphQLRawSDL.swift- ThegraphQLRawSDLglobal property, which is a Swift string literal of the input schema. This is internally used at runtime to parse the schema.GraphQLTypes.swift- Swift protocols and types for your GraphQL types. These are all namespaced withinGraphQLGenerated.
Create a type named GraphQLContext:
actor GraphQLContext {
// Add any features you like
}If your schema has any custom scalar types, you must create them manually in the GraphQLScalars namespace. See the Scalars usage section below for details.
Create a struct that conforms to GraphQLGenerated.Resolvers by defining the required typealiases:
struct Resolvers: GraphQLGenerated.Resolvers {
typealias Query = ExamplePackage.Query
typealias Mutation = ExamplePackage.Mutation
typealias Subscription = ExamplePackage.Subscription
}As you build the Query, Mutation, and Subscription types and their resolution logic, you will be forced to define a concrete type for every reachable GraphQL result, according to its generated protocol:
struct Query: GraphQLGenerated.Query {
// This is required by `GraphQLGenerated.Query`, and used by GraphQL query resolution
static func user(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> (any GraphQLGenerated.User)? {
// You can implement resolution logic however you like
return context.user
}
}
struct User: GraphQLGenerated.User {
// You can define the type internals however you like
let name: String
let email: String
// These are required by `GraphQLGenerated.User`, and used by GraphQL field resolution
func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
return name
}
func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress {
// You can implement resolution logic however you like
return .init(email: self.email)
}
}Let the protocol conformance guide you on what resolver methods your types must define, and keep going until everything compiles.
You're done! You can now instantiate your GraphQL schema by calling buildGraphQLSchema, and run queries against it:
import GraphQL
// Build the auto-generated schema
let schema = try buildGraphQLSchema(resolvers: Resolvers.self)
// Execute a query against it
let result = try await graphql(schema: schema, request: "{ users { name email } }", context: GraphQLContext())
print(result)All generated types other than GraphQLContext and scalar types are namespaced inside of GraphQLGenerated to minimize polluting the inheriting package's type namespace.
Root types (Query, Mutation, and Subscription) are modeled as Swift protocols with static method requirements for each field. The user must implement these types and provide them to the buildGraphQLSchema function via the Resolvers typealiases.
Object types are modeled as Swift protocols with instance method requirements for each field. This is to enable maximum implementation flexibility. Internally, GraphQL passes result objects directly through to subsequent resolvers. By only specifying the interface, we allow the backing types to be incredibly dynamic - they can be simple codable structs or complex stateful actors, reference or values types, or any other type configuration.
Furthermore, by only referencing protocols, we can have multiple Swift types back a particular GraphQL type, and can easily mock portions of the schema. As an example, consider the following schema snippet:
type A {
foo: String
}This would result in the following generated protocol:
protocol A: Sendable {
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
}You could define two conforming types. To use ATest in tests, simply return it from the relevant resolvers.
struct A: GraphQLGenerated.A {
let foo: String
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
return foo
}
}
struct ATest: GraphQLGenerated.A {
func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
return "test"
}
}Also, by not providing default resolvers based on reflection of the type's properties, we improve performance and setup the inheriting package up well for evolving the schema over time.
Interfaces are modeled as a protocol with required methods for each relevant field. Implementing objects and interfaces are marked as requiring conformance to the interface protocol.
Union types are modeled as a marker protocol, with no required properties or functions. Related objects are marked as requiring conformance to the union protocol.
Input object types are modeled as a deterministic Codable struct with the declared fields. If more complex objects must be created from the codable struct, this can be done in the resolver itself, since input objects are only relevant for their associated resolver (they are not passed to downstream resolvers).
Enum types are modeled as a deterministic String enum with values matching the declared fields and associated representations. If you need different values or more complex implementations, simply convert to/from a different representation inside your resolvers.
Scalar types are not modeled by the generator. They are simply referenced as GraphQLScalars.<name>, and you are expected to implement the required type, and conform it to GraphQLScalar. Since GraphQL uses a different serialization system than Swift, you should be sure that the type's conformance to Swift's Codable and GraphQL's GraphQLScalar agree on a representation. Below is an example that represents a scalar struct as a raw String:
extension GraphQLScalars {
struct EmailAddress: GraphQLScalar {
let email: String
init(email: String) {
self.email = email
}
// Codability conformance. Represent simply as `email` string.
init(from decoder: any Decoder) throws {
self.email = try decoder.singleValueContainer().decode(String.self)
}
func encode(to encoder: any Encoder) throws {
try self.email.encode(to: encoder)
}
// Scalar conformance. Parse & serialize simply as `email` string.
static func serialize(this: Self) throws -> Map {
return .string(this.email)
}
static func parseValue(map: Map) throws -> Map {
switch map {
case .string:
return map
default:
throw GraphQLError(message: "EmailAddress cannot represent non-string value: \(map)")
}
}
static func parseLiteral(value: any Value) throws -> Map {
guard let ast = value as? StringValue else {
throw GraphQLError(
message: "EmailAddress cannot represent non-string value: \(print(ast: value))",
nodes: [value]
)
}
return .string(ast.value)
}
}
}- Add Directive support
- Add configuration to reference different
.graphqlsource file locations.