genqlient

repository·main·Indexed 23 days ago

https://github.com/khan/genqlient

A code generation tool for Go that creates type-safe GraphQL clients. It validates GraphQL queries against a schema at compile-time and generates corresponding Go structs for response unmarshaling, eliminating the need for manual struct definitions or the use of interface{}.

Tokens
12.3K
Snippets
39
Records
66
Agent score
79%

What's inside genqlient

  1. What is genqlient?

    main
    genqlient is a Go library designed to generate type-safe code for querying GraphQL APIs. It leverages the type systems of both GraphQL and Go to provide compile-time validation of queries and type-safe response objects. This eliminates the need for manual struct definition or the use of interface{} for unmarshaling results, ensuring that queries are valid against the schema before deployment.
  2. How genqlient handles GraphQL optionality and pointers

    main

    In GraphQL, types can be optional (nullable) or non-optional (e.g., String!). Because Go does not have a native way to distinguish between a zero value (like "") and a null value for basic types, libraries often use pointers (*string).

    genqlient's approach: By default, genqlient does not use pointers for optionality to maintain idiomatic Go style. This means it may be difficult to distinguish between a field being set to its zero value versus being null. However, the library is designed to allow for future configuration options to enable pointers (for distinguishing zero vs. null) or presence fields.

  3. Understand genqlient breaking changes and versioning

    main

    Genqlient follows semantic versioning, but because it is currently in 0.x versioning, breaking changes may occur. The project aims to limit breaking changes to minor version bumps (e.g., 0.6.0 instead of 0.5.1).

    What constitutes a breaking change?

    • Breaking changes to the runtime graphql package.
    • Changes that alter the API or behavior of the generated code for the same valid query (re-running a newer version of genqlient on existing queries should be safe).
    • Changes to the graphql runtime package that require corresponding changes to the code-generator.

    What is NOT considered a breaking change?

    • Syntactic changes to generated output (if you use CI to check that generated code is up to date, you should expect to update it when upgrading genqlient).
    • Breaking changes to double-underscore-prefixed names (__name) in generated code or names in the graphql runtime documented as "intended for the use of genqlient's generated code only".
    • Changes to the code-generator that require corresponding changes to the graphql runtime package.
    • Dropping support for Go versions that are no longer supported by the Go project.

    Runtime Compatibility

    Your version of the graphql runtime package must be the same major version and the same or newer than your version of the code-generator. It is recommended to use the same version for both.

  4. Map GraphQL scalars to Go types using bindings

    main

    By default, genqlient maps standard GraphQL scalars to Go types as follows:

    GraphQL typeGo type
    Intint
    Floatfloat64
    Stringstring
    Booleanbool
    IDstring

    To handle custom scalars or to override these defaults (e.g., changing Int to int32), use the bindings option in your genqlient.yaml file.

  5. Work with GraphQL Interfaces in Go

    main

    When querying an interface, genqlient generates a Go interface for the interface type and specific structs for each implementation.

    • Accessing shared fields: Use the methods defined on the generated interface.
    • Accessing type-specific fields: Use a Go type switch to assert the interface to a specific implementation struct.

    If you only want to request shared fields and do not need fragments, you can use # @genqlient(struct: true) on the interface field to skip interface generation and just get a plain struct.

    // Using type switches for interface implementations
    resp, err := GetBooks(...)
    fmt.Println("Favorite book:", resp.Favorite.GetTitle())
    if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok {
      fmt.Println("Protagonist:", novel.Protagonist)
    }
  6. Understand genqlient response objects

    main

    Every generated helper function returns a pointer to a struct that mirrors the shape of your GraphQL query result. For example, a query for a user with a name field will generate a response struct containing a User field, which in turn contains a Name string.

    // Example generated structure for:
    // query getUser($login: String!) { user(login: $login) { name } }
    
    func getUser(...) (*getUserResponse, error) { ... }
    
    type getUserResponse struct {
    	User getUserUser
    }
    
    type getUserUser struct {
    	Name string
    }
  7. Handle nullable fields using Zero Values, Pointers, or Generics

    main

    By default, genqlient maps GraphQL null values to Go zero values (e.g., null string becomes ""). If you need to distinguish between a null value and a zero value, you have three options:

    1. Zero Values (Default): Maps null to the type's zero value (e.g., 0 for int, "" for string). For structs, the entire struct is set to its zero value.
    2. Pointers: Use the @genqlient(pointer: true) directive to map null to nil. This generates a pointer type (e.g., *string).
    3. Generics: Use a custom generic type (similar to Rust's Option<T>) by configuring optional: generic and optional_generic_type in your genqlient.yaml. This generates a type like Option[string].
    # genqlient.yaml configuration for generics
    optional: generic
    optional_generic_type: github.com/path/to/your/package.Option
  8. How genqlient supports GraphQL fragments

    main

    genqlient handles fragments using two different strategies depending on whether the fragment is inline or named:

    1. Inline Fragments (... on T { ... }): These are flattened into the parent struct. This simplifies the resulting types and makes them easier to use without extra layers of nesting.
    2. Named Fragments (fragment F on T { ... }): These are embedded as separate structs. This allows for better code deduplication; if a fragment is used in multiple places, you can write a function that accepts the fragment's type as an argument.

    Summary of strategies:

    • Flattening: Best for inline fragments; results in simpler, flatter structs.
    • Embedding: Best for named fragments; allows for type reuse and cleaner naming roots.
  9. Configure query function signatures (Context and Client)

    main

    Genqlient allows you to configure how the generated query functions are structured, specifically regarding how they handle context.Context and the GraphQL Client.

    You can configure the following:

    • Context usage: Specify whether to use no context, a specific custom context type, or the default context.Context.
    • Client retrieval: Specify how to obtain the graphql.Client. This can be via a global client or by providing a hook/function to retrieve the client from the context (e.g., using context.Value or a custom method).

    This flexibility allows the generated code to integrate with various dependency injection patterns or custom context implementations used in your project.

    // Example of possible generated signatures:
    
    // Uses a standard context
    func GetUser(ctx context.Context, id string) (*GetUserResponse, error)
    
    // Uses a custom context
    func GetUser(ctx mypkg.Context, id string) (*GetUserResponse, error)
    
    // Uses a client object
    func GetUser(client graphql.Client, id string) (*GetUserResponse, error)
    
    // Uses both
    func GetUser(ctx context.Context, client graphql.Client, id string) (*GetUserResponse, error)
  10. How genqlient represents GraphQL interfaces

    main

    When a GraphQL query returns an interface, genqlient uses Go interfaces to represent the result. This is considered the most natural translation of GraphQL to Go.

    How it works:

    1. A Go interface is generated for the GraphQL interface (e.g., type I interface { isI() }).
    2. Every concrete GraphQL type that implements that interface gets its own Go struct (e.g., type T struct { ... }).
    3. These concrete structs implement the interface by providing the required methods (e.g., func (t T) isI() {}).

    Usage: To access fields on an interface, you typically use a type switch to determine the concrete type, or you can use getter methods if they are provided by the interface.

    // Example of interface representation
    // GraphQL: interface I { b: String } | type T implements I { b: String, c: String } | type U implements I { b: String }
    
    type I interface {
        isI()
        GetB() string
    }
    
    type T struct {
        B string
        C string
    }
    func (t T) isI() {}
    func (t T) GetB() string { return t.B }
    
    type U struct {
        B string
    }
    func (u U) isI() {}
    func (u U) GetB() string { return u.B }
    
    type Response struct {
        A I
    }
  11. Customizing Go field names using GraphQL aliases

    main

    By default, genqlient uses the schema's field names for Go struct fields. You can customize these names by using GraphQL field aliases. genqlient will uppercase the alias to ensure the field is exported and visible to the Go JSON library.

    Example: Using myGreatName: myString in your query will result in a Go field named MyGreatName.

    query MyQuery {
      myGreatName: myString
    }
  12. Comparison: Traditional GraphQL clients vs genqlient

    main

    In traditional Go GraphQL clients, developers often manually define query strings and response structs. This approach has several risks:

    1. Schema Mismatch: The Go struct might not match the actual GraphQL schema (e.g., wrong field names or casing), which is only discovered at runtime.
    2. Variable Type Safety: GraphQL variables are often passed as map[string]interface{}, allowing for type errors that only surface at runtime.
    3. Boilerplate: Developers must write both the query and the corresponding struct, often requiring complex struct tags.

    genqlient solves this by allowing you to simply specify the query; it then automatically validates the query against the schema and generates the necessary type-safe Go helpers and structs.

    // Example of the manual, less safe approach used by other clients:
    query := `query GetUser($id: ID!) { user(id: $id) { name } }`
    variables := map[string]interface{}{"id": "123"}
    var resp struct {
    	Me struct {
    		Name graphql.String
    	}
    }
    client.Query(ctx, query, &resp, variables)
    fmt.Println(resp.Me.Name)