Skip to content

GraphQLSwift/graphql-generator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WARNING: This package is in v0.x beta. It's API is still evolving and is subject to breaking changes in minor version bumps.

GraphQL Generator for Swift

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.

Features

  • Data-driven: Guarantee conformance with the declared GraphQL spec
  • Type-safe: Leverages Swift's type system for compile-time safety
  • Flexible implementation: Makes no assumptions about backing data types other than GraphQL type conformance
  • Minimal boilerplate: Generates all the piping between Swift and GraphQL - you just write the resolvers

Installation

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")
        ]
    )
]

Quick Start

Take a look at the example projects to see real, fully featured implementations:

  • HelloWorldServer - Demonstrates all GraphQL type mappings with a comprehensive schema
  • StarWars - A production-like example using the SWAPI with DataLoader for caching

1. Create a GraphQL Schema

Create a .graphql file in your target's Sources directory:

Sources/ExamplePackage/schema.graphql:

type User {
  name: String!
  email: EmailAddress!
}

type Query {
  user: User
}

2. Build Your Project

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 - Defines buildGraphQLSchema function that builds an executable schema.
  • GraphQLRawSDL.swift - The graphQLRawSDL global property, which is a Swift string literal of the input schema. This is used at runtime to parse the schema.
  • GraphQLTypes.swift - Swift protocols and types for your GraphQL types. These are all namespaced within GraphQLGenerated.

3. Create required types

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 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 type, 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.

4. Execute GraphQL Queries

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)

Design Philosophy

This generator is designed with the following guiding principles:

  • Protocol-based flexibility: GraphQL types are generated as Swift protocols (except where concrete types are needed), allowing you to implement backing types however you want - structs, actors, classes, or any combination.
  • Explicit over implicit: No default resolvers based on reflection. While more verbose, this provides better performance and clearer schema evolution handling.
  • Type safety: Leverage Swift's type system to ensure compile-time conformance with your GraphQL schema.
  • Namespace isolation: All generated types (except GraphQLContext and custom scalars) are namespaced inside GraphQLGenerated to avoid polluting your package's type namespace.

GraphQL to Swift Type Mappings

This section describes how each GraphQL type is converted to Swift code, with concrete examples from the HelloWorldServer example. Note that all generated types are namespaced inside GraphQLGenerated

Root Types (Query, Mutation, Subscription)

GraphQL root types are generated as Swift protocols with static methods for each field.

GraphQL:

type Query {
  user(id: ID!): User
  users: [User!]!
}

type Mutation {
  upsertUser(userInfo: UserInfo!): User!
}

type Subscription {
  watchUser(id: ID!): User
}

Generated Swift:

protocol Query: Sendable {
    static func user(id: String, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> (any User)?
    static func users(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> [any User]
}

protocol Mutation: Sendable {
    static func upsertUser(userInfo: UserInfo, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> any User
}

protocol Subscription: Sendable {
    static func watchUser(id: String, context: GraphQLContext, info: GraphQLResolveInfo) async throws -> AnyAsyncSequence<(any User)?>
}

Object Types

GraphQL object types are generated as Swift protocols with instance methods for each field. This allows for flexible implementations - you can use structs, actors, classes, or any other type that conforms to the protocol.

GraphQL:

type User {
  id: ID!
  name: String!
  email: EmailAddress!
  age: Int
}

Generated Swift:

protocol User: Sendable {
    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
    func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int?
}

Example Implementation:

struct User: GraphQLGenerated.User {
    let id: String
    let name: String
    let emailAddress: String
    let age: Int?

    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
        return id
    }
    func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String {
        return name
    }
    func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress {
        return .init(email: emailAddress)
    }
    func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int? {
        return age
    }
}

Because these are protocols, you can have multiple implementations of the same GraphQL type (useful for testing or different data sources):

struct MockUser: GraphQLGenerated.User {
    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String { "test-id" }
    func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String { "Test User" }
    func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress {
        .init(email: "test@example.com")
    }
    func age(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> Int? { nil }
}

Interface Types

GraphQL interfaces are generated as Swift protocols with required methods for each field. Types implementing the interface will have their protocol marked as conforming to the interface protocol.

GraphQL:

interface HasEmail {
  email: EmailAddress!
}

type User implements HasEmail {
  id: ID!
  name: String!
  email: EmailAddress!
}

Generated Swift:

protocol HasEmail: Sendable {
    func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
}

protocol User: HasEmail, Sendable {
    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func email(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> GraphQLScalars.EmailAddress
}

Union Types

GraphQL union types are generated as Swift marker protocols with no required properties or methods. Union member types have their protocols marked as conforming to the union protocol.

GraphQL:

union UserOrPost = User | Post

type User {
  id: ID!
  name: String!
}

type Post {
  id: ID!
  title: String!
}

Generated Swift:

protocol UserOrPost: Sendable {}

protocol User: UserOrPost, Sendable {
    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func name(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
}

protocol Post: UserOrPost, Sendable {
    func id(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
    func title(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String
}

Input Object Types

GraphQL input object types are generated as concrete Swift structs with properties for each field. These are Codable and Sendable.

GraphQL:

input UserInfo {
  id: ID!
  name: String!
  email: EmailAddress!
  age: Int
  role: Role = USER
}

Generated Swift:

struct UserInfo: Codable, Sendable {
    let id: String
    let name: String
    let email: GraphQLScalars.EmailAddress
    let age: Int?
    let role: Role?
}

Enum Types

GraphQL enum types are generated as concrete Swift enums with raw String values. Each GraphQL enum case becomes a Swift enum case with its raw value matching the GraphQL case name.

GraphQL:

enum Role {
  ADMIN
  USER
  GUEST
}

Generated Swift:

enum Role: String, Codable, Sendable {
    case admin = "ADMIN"
    case user = "USER"
    case guest = "GUEST"
}

These generated enums can be used directly in your code without any additional implementation.

Scalar Types

GraphQL scalar types are not generated by the plugin. Instead, they are referenced as GraphQLScalars.<name>, and you must define the type and conform it to GraphQLScalar.

GraphQL:

scalar EmailAddress

type User {
  email: EmailAddress!
}

Required Implementation:

extension GraphQLScalars {
    struct EmailAddress: GraphQLScalar {
        let email: String

        init(email: String) {
            self.email = email
        }

        // Codable conformance - for Swift serialization
        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)
        }

        // GraphQLScalar conformance - for GraphQL serialization
        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)
        }
    }
}

Ensure that your Codable and GraphQLScalar conformances agree on the same representation format.

About

GraphQL SDL -> Schema in Swift

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages