Cynic

A bring your own types GraphQL client for Rust

Crate Info API Docs Discord Chat

Repository | Examples | Changelog

Overview

Cynic is a GraphQL library for Rust. It's not the first but it takes a different approach from the existing libraries.

Existing libraries take a query first approach to GQL - you write a query using GraphQL and libraries use that to generate Rust structs for you using macros. This is really easy and great for getting going quickly. However, if you want to use structs that aren't quite what the macros output you're out of luck. Some more complex use cases like sharing structs among queries are also commonly not supported.

Cynic takes a different approach - it uses Rust structs to define queries and generates GraphQL from them. This gives you freedom to control the structs you'll be working with while still enjoying type safe queries, checked against the GraphQL schema. When it's built in derives don't do exactly what you want it provides lower level APIs to hook in and fetch the data you want in the format you want to work with it.

Of course writing out all the structs to represent a large GraphQL query can be quite challenging, and GraphQL has excellent tooling for building queries usually. Cynic provides querygen to help with this - you write a GraphQL query using the existing GQL tooling and it'll generate some cynic structs to make that query. You can use this as a starting point for your projects - either adding on to the rust structs directly, or re-using querygen as appropriate.

Features

Cynic is currently a work in progress, but the following features are supported:

  • Typesafe queries & mutations.
  • Defining custom scalars.
  • Building dynamic (but still type checked) queries at run time.
  • Query arguments including input objects
  • Interfaces & union types
  • GraphQL Subscriptions via graphql-ws-client. (though graphql-ws-client is fairly alpha quality)

The following features are not yet supported, though should be soon (if you want to help out with the project I'd be happy for someone else to try and implement these - if you open an issue I'd be happy to give pointers on how to go about implementing any of them)

  • Directives
  • Potentially other things (please open an issue if you find anything obviously missing)

Documentation

Cynic is documented in a few places:

  1. The guide that you're reading on cynic-rs.dev
  2. The reference documentation on docs.rs

Using This Guide

If you're new to Cynic the quickstart is a good place to start. Afterwards you might want to read the derives chapter, for more details about how to do common things with Cynic.

The Building Queries section is for more advanced users - either you've run into a case that the built in derives don't cover, or you're just curious how things work under the hood.

Inspiration

  • graphql-client, the original Rust GraphQL client. This is a great library for using GraphQL from Rust. It wasn't quite what I wanted but it might be what you want.
  • The idea of encoding the GraphQL typesystem into a DSL was taken from elm-graphql.
  • Most of the JSON decoding APIs were taken from Json.Decode in Elm.
  • Deriving code from structs is a fairly common Rust pattern, though serde in particular provided inspiration for the derive APIs.

Quickstart

If you just want to get going with cynic and don't care too much about how it works, this is the chapter for you.

Pre-requisites

There's a few things you'll need before you get started:

  1. An existing rust project (though you can just run cargo new if you don't have one).
  2. A GraphQL API that you'd like to query, and a copy of it's schema.
  3. A GraphQL query that you'd like to run against the API. If you don't have one of these, you should probably use graphiql or graphql playground to get started, or you can use the one I provide below. For this quickstart I'll be assuming you're using a query without any arguments.

Setting up dependencies.

First things first: you need to add cynic to your dependencies. We'll also need an HTTP client library. For the purposes of this quickstart we'll be using surf but you can use any library you want. Open up your Cargo.toml and add the following under the [dependencies] section:

cynic = { version = "2", features = ["http-surf"] }
surf = "2"

Note that we've added the http-surf feature flag of cynic - this pulls in some surf integration code, which we'll be using. If you're using a different HTTP client, you'll need a different feature flag or you may need to see the documentation for making an HTTP request manually.

You may also optionally want to install insta - a snapshot testing library that can be useful for double checking your GraphQL queries are as expected. Add this under [dev-dependencies] so it's available under test but not at runtime:

[dev-dependencies]
insta = "1"

Run a cargo check to make sure this builds and you're good to go.

Adding your schema to the build.

You'll want to make sure the GraphQL schema for your build is available to your builds. For example, you could put it at src/schema.graphql - the rest of this tutorial will assume that's where you put the schema.

Building your query structs.

Cynic allows you to build queries from Rust structs - so you'll need to take the query you're wanting to run and convert it into some rust structs. This can be quite laborious and error prone for larger queries so cynic provides a generator to help you get started.

Go to https://generator.cynic-rs.dev and select how you'd like to input your schema. If the GraphQL API you wish to use is accessible on the internet you can just link directly to it (although it will need to have CORS headers enabled). Otherwise you can upload your schema to the generator.

Once you've provided the schema, you should be dropped into a GraphiQL interface but with an extra panel that contains Rust generated from your query & schema.

Checking your query (optional)

Since cynic generates queries for you based on Rust structs, it's not always obvious what the GraphQL queries look like. Sometimes you might want to run a query manually via Graphiql, or you might just want to see what effects changing the rust structs have on the query itself.

I find writing snapshot tests using insta useful for this purpose. Assuming your query is called AllFilmsQuery like mine is, you can add the following to the same file you put the struct output into:


#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_films_query_gql_output() {
        use cynic::QueryBuilder;

        let operation = AllFilmsQuery::build(());

        insta::assert_snapshot!(operation.query);
    }
}
}

You can now run this test with cargo test. It should fail the first time, as you've not yet saved a snapshot. Run cargo insta review (you may need to cargo install insta first) to approve the snapshot, and the test should succeed.

You can use this snapshot test to double check the query whenever you make changes to the rust code, or just when you need some actual GraphQL to make a query outside of cynic.

Making your query

Now, you're ready to make a query against a server. Cynic provides integrations for the surf & reqwest HTTP clients. We'll use surf here, but it should be similar for reqwest (or if you're using another HTTP library see here for how to use cynic with it).

First, you'll want to build an Operation similar to how we did it in the snapshot test above (again, swapping AllFilmsQuery for the name of your root query struct):


#![allow(unused)]
fn main() {
use cynic::{QueryBuilder, http::SurfExt};

let operation = AllFilmsQuery::build(());
}

This builds an Operation struct with is serializable using serde::Serialize. You should pass it in as the HTTP body using your HTTP client and then make a request. For example, to use surf to talk to the StarWars API (see the docs for cynic::http if you're using another client):


#![allow(unused)]
fn main() {
let response = surf::post("https://swapi-graphql.netlify.app/.netlify/functions/index")
    .run_graphql(&operation)
    .await
    .unwrap();
}

Now, assuming everything went well, you should have the response to your query which you can do whatever you want with. And that's the end of the quickstart.

Deriving GraphQL Queries

Cynic has a few layers to it. The highest level of these is it's derives - using these derives you can define GraphQL queries using structs and enums, and have them type checked against the GraphQL schema itself.

There are a number of different derives which this section details:

Query Fragments

QueryFragments are the main tool for building queries & mutations in cynic. Cynic builds up a GraphQL query document from the fields on a QueryFragment and any QueryFragments nested inside it. And after executing an operation it deserializes the result into the QueryFragment struct.

Generally you'll use a derive to create query fragments, like this:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    schema_module = "schema",
)]
struct Film {
    title: Option<String>,
    director: Option<String>,
}
}

When this struct is used in a query it will select the title & director fields of the Film type, which are both optional strings. QueryFragments can be nested inside each other, like so:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    schema_module = "schema",
    graphql_type = "Root",
)]
struct FilmsConnection {
    films: Vec<Film>,
}
}

If the above QueryFragment was used in a query, it would result in GraphQL that looked like:

films {
  title
  director
}

QueryFragments are compile time checked against the provided GraphQL schema. You cannot nest a Film QueryFragment into a field that was expecting an Actor for example. Similarly, nullable fields must be an Option and lists must be a Vec.

Making a Query with QueryFragments

QueryFragments that apply to the Query type (otherwise known as the Root type) of a schema can be used to build a cynic::Operation. This Operation is the type that should be serialized and sent to the server.

If we wanted to use our FilmConnection to get all the films from the star wars API we need a QueryFragment like this:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    schema_module = "schema",
    graphql_type = "Root",
)]
struct AllFilmsQuery {
    all_films: Option<FilmConnection>,
}
}

An Operation can be created from this QueryFragment:


#![allow(unused)]
fn main() {
use cynic::QueryBuilder;

let operation = AllFilmsQuery::build(());
}

This particular query has no arguments so we provide the unit type () in place of actual arguments.

This Operation can be serialized into JSON using serde, sent to a server, and then then a cynic::GraphQlResponse<AllFilmsQuery> can be deserialized from the response. An example of this is in the Quickstart.

Passing Arguments

GraphQL allows a server to define arguments that a field can accept. Cynic provides support for passing in these arguments via its arguments attribute.

Here, we define a query that fetches a film by a particular ID:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    schema_module = "schema",
    graphql_type = "Root",
)]
struct FilmQuery {
    #[arguments(id: "ZmlsbXM6MQ==")]
    film: Option<Film>,
}
}

Note the #[arguments: id: "ZmlsbXM6MQ=="] attribute on the film field. The GraphQL generated for this query will provide a hard coded id argument to the film field, like this:

film(id: "ZmlsbXM6MQ==") {
  title
  director
}

The syntax of the inside of arguments is very similar to the syntax expected for arguments in GraphQL itself. Some examples:

GraphQLCynic
input: { filters: "ACTIVE" }input: { filters: "ACTIVE" }
values: ["Hello"]values: ["Hello"]
values: ["Hello"]values: ["Hello"]
arg1: "Foo", arg2: "Bar"arg1: "Foo", arg2: "Bar"
`arg1: nullarg1: null

Variables

If you don't want to hard code the value of an argument, you can parameterise your query with some variables. These variables must be defined on a struct:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryVariables)]
struct FilmQueryVariables {
    id: Option<cynic::Id>,
}
}

The fields of this struct can be any Enum, InputObject, or Scalar.

To use this struct you need to tell your QueryFragment that it takes variables using the variables parameter to to the cynic attribute, and then you can use variables much like you would in GraphQL.

Here, we update our FilmQuery struct to make use of our FilmQueryVariables to provide the id argument.


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    schema_module = "schema",
    graphql_type = "Root",
    variables = "FilmQueryVariables"
)]
struct FilmQuery {
    #[arguments(id: $id)]
    film: Option<Film>,
}
}

Any field of the variables struct may be used by prefixing the name of the field with $.

This can be converted into a query in a similar way we just need to provide some FilmArguments:


#![allow(unused)]
fn main() {
use cynic::QueryBuilder;

let operation = FilmQuery::build(
    FilmArguments{
        id: Some("ZmlsbXM6MQ==".into()),
    }
);
}

See query variables for more details.

Nested Variables

The example above showed how to pass variables to the top level of a query. If you want to pass variables to a nested QueryFragment then all it's parent QueryFragments must specify the same variables in their cynic attribute. This is necessary so that the QueryVariables struct gets passed down to that level of a query.

If no nested QueryFragments require arguments, you can omit the variables attr from those QueryFragments

Mutations

Mutations are also constructed using QueryFragments in a very similar way to queries. Instead of selecting query fields you select a mutation, and pass in any arguments in exactly the same way. Mutations use the MutationBuilder rather than QueryBulder:


#![allow(unused)]
fn main() {
use cynic::MutationBuilder;

let operation = SomeMutation::build(SomeArguments { ... });
}

This operation can then be used in exactly the same way as with queries.

Struct Attributes

A QueryFragment can be configured with several attributes on the struct itself:

  • graphql_type = "AType" tells cynic which object in the GraphQL schema this struct represents. The name of the struct is used if it is omitted.
  • schema_path sets the path to the GraphQL schema. This is required, but can be provided by nesting the QueryFragment inside a query module with this attr.
  • schema_module tells cynic where to find the schema module - that is a module module that has called the use_schema! macro. This will default to schema if not provided. An override can also be provided by nesting the QueryFragment inside a module with the schema_for_derives attribute macro.
  • variables defines the QueryVariables struct that is available to arguments attributes on fields of the given struct.

Field Attributes

Each field can also have it's own attributes:

  • rename = "someGraphqlName" can be provided if you want the rust field name to differ from the GraphQL field name. You should provide the name as it is in the GraphQL schema (although due to implementation details a snake case form of the name may work as well)
  • alias can be provided if you have a renamed field and want to explicitly request a GraphQL alias in the resulting query output.
  • recurse = "5" tells cynic that this field is recursive and should be fetched to a maximum depth of 5. See Recursive Queries for more info.
  • The flatten attr can be used to "flatten" out excessive Options from lists. As GraphQL is used in languages with implicit nulls, it's not uncommon to see a type [Int] - which in Rust maps to Option<Vec<Option<i32>>. This isn't a very nice type to work with - applying the flatten attribute lets you represent this as a Vec<i32> in your QueryFragment. Any outer nulls become an empty list and inner nulls are dropped.
  • The spread attr can be used to spread another QueryFragments into the current QueryFragment, if each of the QueryFragments point at the same GraphQL type.

Enums

Much like with query structs cynic expects you to own any enums you want to query for, or provide as arguments. The cynic::Enum trait is used to define an enum, and the easiest way to define that trait is to derive it:


#![allow(unused)]
fn main() {
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum Market {
    Uk,
    Ie,
}
}

The derive will work on any enum that only has unit variants that match up with the variants on the enum in the schema. If there are any extra or missing variants, the derive will emit errors.

Variant Naming

The GraphQL spec recommends that enums are "all caps". To handle this smoothly, Cynic matches rust variants up to their equivalent SCREAMING_SNAKE_CASE GraphQL variants. This behaviour can be disabled by specifying a rename_all = "None" attribute, or customised alternative rename_all values or individual rename attributes on the variants.

Enum Attributes

An Enum can be configured with several attributes on the enum itself:

  • graphql_type = "AType" tells cynic which enum in the GraphQL schema this enum represents. The name of the enum is used if it is omitted.
  • rename_all="camelCase" tells cynic to rename all the rust field names with a particular rule to match their GraphQL counterparts. If not provided this defaults to SCREAMING_SNAKE_CASE to be consistent with GraphQL conventions.
  • schema_module tells cynic where to find the schema module - that is a module that has called the use_schema! macro. This will default to schema if not provided. An override can also be provided by nesting the Enum inside a module with the schema_for_derives attribute macro.

Variant Attributes

Each variant can also have it's own attributes:

  • rename="SOME_VARIANT" can be used to map a variant to a completely different GraphQL variant name.

Scalars

Cynic supports all the built in GraphQL scalars by default. If you want to query a field of one of these types add a field of the corresponding Rust type to your QueryFragment struct.

  • String fields in GraphQL should be String fields in Rust.
  • Int fields in GraphQL should be i32 in Rust.
  • Boolean fields in GraphQL map to bool in Rust.
  • ID fields in GraphQL map to the cynic::Id type in Rust.

Custom Scalars

impl_scalar!

GraphQL allows a schema to define it's own scalars - cynic also supports these.

If you have an existing type (including 3rd party types) that has a serde::Serialize impl and want to use it as a Scalar, you can use impl_scalar! to register it as a Scalar. For example to register chrono::DateTime<chrono::Utc> as a DateTime scalar:


#![allow(unused)]
fn main() {
type DateTime = chrono::DateTime<chrono::Utc>;
impl_scalar!(DateTime, schema::DateTime);
}

This DateTime type alias can now be used anywhere that the schema expects a DateTime. Note that the type alias is currently required due to limitations in some of the cynic macros (though this may not always be the case).

Note that this impl_scalar call must live in either the crate that defines the type or the crate that contains the schema module.

#[derive(Scalar)]

You can also derive Scalar on any newtype structs:


#![allow(unused)]
fn main() {
#[derive(cynic::Scalar, serde::Serialize)]
#[cynic(schema_module = "schema")]
struct MyScalar(String);
}

This MyScalar type can now be used anywhere the schema expects a MyScalar.

Any types that derive cynic::Scalar must also derive (or otherwise implement) serde::Serialize. You can change the inner type that's used to deserialize the scalar by changing the type inside the struct.

Note that this derive only works on newtype structs - for any more complex datatype you'll have to implement cynic::Scalar yourself.

Struct Attributes

A Scalar derive can be configured with several attributes on the struct itself:

  • graphql_type = "AType" can be provided if the type of the struct differs from the type of and tells cynic the name of the Scalar in the schema. This defaults to the name of the struct if not provided.
  • schema_module tells cynic where to find the query module - that is a module that has called the use_schema! macro. This is required but can also be provided by nesting the QueryFragment inside a query module.

Variables

A hierarchy of QueryFragments can take a struct of variables. This struct must implement QueryVariables which can be derived:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryVariables)]
struct FilmVariables {
    id: Option<cynic::Id>,
}
}

Using QueryVariables

To use any fields of this struct as an argument to a QueryFragment, the struct must provide a variables parameter that points to the FilmArguments struct. This allows variables to be passed in using the arguments attribute on the fields where you wish to pass them.


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
#[cynic(
    schema_path = "examples/starwars.schema.graphql",
    graphql_type = "Root",
    variables = "FilmVariables"
)]
struct FilmQuery {
    #[arguments(id: $id)]
    film: Option<Film>,
}
}

This example uses our FilmVariables at the root of the query to specify which film we want to fetch.

It's also possible to pass variables down to lower levels of the query using the same technique. Though it's worth noting that all the QueryFragments from the Root to the point that requires arguments must define the same variables in their cynic attribute. If no nested QueryFragments require any variables then it's OK to omit variables.

InputObject

Some fields in GraphQL expect you to provide an input object rather than just simple scalar types. The cynic::InputObject trait is used to define an input object and the easiest way to define that trait is to derive it:


#![allow(unused)]
fn main() {
#[derive(cynic::InputObject, Clone, Debug)]
pub struct IssueOrder {
    pub direction: OrderDirection,
    pub field: IssueOrderField,
}
}

The derive will work on any struct that matches the format of the input object and it may contain scalar values or other InputObjects. See Field Naming below for how names are matched between GraphQL & Rust.

By default the field names are expected to match the GraphQL variants exactly, but this can be controlled with either the rename_all top level attribute or the rename attribute on individual fields.

If there are any fields in the struct that are not on the GraphQL input type the derive will emit errors. Any required fields that are on the GraphQL input type but not the rust struct will also error. Optional fields may be omitted without error. This maintains the same backwards compatibility guarantees as most GraphQL clients: adding a required field is a breaking change, but adding an optional field is not.

Currently any missing optional fields will not be serialized in queries, whereas optional fields that are present in the struct but set to None will be sent as null.

Field Naming

It's a common GraphQL convention for fields to be named in camelCase. To handle this smoothly, Cynic matches rust fields up to their equivalent camelCase GraphQL fields. This behaviour can be disabled by specifying a rename_all = "None" attribute, or customised via alternative rename_all values or individual rename attributes on the fields.

Struct Attributes

An InputObject can be configured with several attributes on the struct itself:

  • graphql_type = "AType" tells cynic which input object in the GraphQL schema this struct represents. The name of the struct is used if it is omitted.
  • require_all_fields can be provided when you want cynic to make sure your struct has all of the fields defined in the GraphQL schema.
  • rename_all="camelCase" tells cynic to rename all the rust field names with a particular rule to match their GraphQL counterparts. If not provided this defaults to camelCase to be consistent with GraphQL conventions.
  • schema_module tells cynic where to find the schema module - that is a module that has called the use_schema! macro. This will default to schema if not provided. An override can also be provided by nesting the InputObject inside a module with the schema_for_derives attribute macro.

Field Attributes

Each field can also have it's own attributes:

  • rename="someFieldName" can be used to map a field to a completely different GraphQL field name.
  • skip_serializing_if="path" can be used on optional fields to skip serializing them. By default an Option field will be sent as null to servers, but if you provide skip_serializing_if="Option::is_none" then the field will not be provided at all.

Inline Fragments

InlineFragments are used when querying an interface or union type, to determine which of the sub-types the query returned and to get at the fields inside that type.

InlineFragments can be derived on any enum that's variants contain a single type that implements QueryFragment. Each of the QueryFragments should have a graphql_type that maps to one of the sub-types of the graphql_type type for the InlineFragment.

For example, the GitHub API has an Assignee union type which could be queried with:


#![allow(unused)]
fn main() {
#[derive(cynic::InlineFragments)]
#[cynic(schema_path = "github.graphql")]
enum Assignee {
    Bot(Bot),
    Mannequin(Mannequin)
    Organization(Organization),
    User(User)

    #[cynic(fallback)]
    Other
}
}

Where each of Bot, Mannequin, Organization & User are all structs that implement QueryFragment for the respective GraphQL types.

Fallbacks

Cynic requires a fallback variant on each InlineFragments that will be matched when the server returns a type other than the ones you provide. This allows your code to continue compiling & running in the face of additions to the server, similar to the usual GraphQL backwards compatibility guarantees.


#![allow(unused)]
fn main() {
#[derive(cynic::InlineFragments)]
#[cynic(schema_path = "github.graphql")]
enum Assignee {
    Bot(Bot),
    User(User)

    #[cynic(fallback)]
    Other
}
}
Fallbacks for interfaces

If your InlineFragments is querying an interface your fallback variant can also select some fields from the interface:


#![allow(unused)]
fn main() {
#[derive(cynic::InlineFragments, Debug)]
#[cynic(schema_path = "github.graphql")]
pub enum Actor {
    User(User),

    #[cynic(fallback)]
    Other(ActorFallback),
}

#[derive(cynic::QueryFragment)]
#[cynic(schema_path = "github.graphql")]
enum ActorFallback {
    pub login: String
}
}

This functionality is only available for interfaces as union types have no concept of shared fields.

Fallbacks for unions

If your InlineFragments is querying a union your fallback variant can receive the __typename of the type that was received. In the case below, if the Assignee is not a Bot or a User, the String field of Other will be populated with the name of the type (the __typename) that was actually received from the server.


#![allow(unused)]
fn main() {
#[derive(cynic::InlineFragments)]
#[cynic(schema_path = "github.graphql")]
enum Assignee {
    Bot(Bot),
    User(User)

    #[cynic(fallback)]
    Other(String)
}
}

This functionality is currently only available for unions.

Struct Attributes

An InlineFragments can be configured with several attributes on the enum itself:

  • graphql_type = "AType" tells cynic which interface or union type in the GraphQL schema this enum represents. The name of the enum is used if it is omitted.
  • schema_path sets the path to the GraphQL schema. This is required, but can be provided by nesting the InlineFragments inside a query module with this attr.
  • schema_module tells cynic where to find the schema module - that is a module that has called the use_schema! macro. This will default to schema if not provided. An override can also be provided by nesting the InlineFragments inside a module with the schema_for_derives attribute macro.

Variant Attributes

Each variant can also have it's own attributes:

  • fallback can be applied on a single variant to indicate that it should be used whenever cynic encounters a __typename that doesn't match one of the other variants. For interfaces this can contain a QueryFragment type. For union types it must be applied on a unit variant.

Recursive Queries

GraphQL allows for types to recurse and allows queries of those recursive types to a particular depth. Cynic supports this in it's QueryFragment derive when users provide the recurse attribute on the field that recurses.

If we wanted to recursively fetch characters that were in a star wars film and the other films that they were in (and so on) we could do:


#![allow(unused)]
fn main() {
#[derive(cynic::QueryFragment, Debug)]
struct Film {
    title: Option<String>,
    character_connection: Option<CharacterConnection>,
}


#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "FilmCharacterConnection")]
struct CharacterConnection {
    characters: Option<Vec<Option<CharacterConnection>>>,
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Person")]
struct Character {
    name: Option<String>,
    films: Option<PersonFilmsConnection>
}

#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Person")]
struct PersonFilmsConnection {
    #[cynic(recurse = "5")]
    films: Option<Vec<Option<Film>>>
}
}

The #[cynic(recurse = "5")] attribute on films in PersonFilmsConnection lets cynic know that it should recursively fetch films to a maximum depth of 5.

Recursive Field Types

When the recurse attribute is present on a field, the rust type that field is required to be may change from a non-recursive field.

  • If the field is not nullable it will need to be wrapped in Option to account for the case where we finish recursing.
  • If the field is nullable but not inside a list, it's normal Option will need to be wrapped in a Box, as this is required for recursive types in Rust.
  • If the field is not nullable and not inside a list, it will also need a Box. This box goes inside the Option.

Some examples of the changes:

GraphQL TypeRust Type Without RecurseRust Type With Recurse
TOption<T>Box<Option<T>>
T!TOption<Box<T>>
[T]Option<Vec<Option<T>>>Option<Vec<Option<T>>>
[T]!Vec<Option<T>>Option<Vec<Option<T>>>
[T!]Option<Vec<T>Option<Vec<T>>

Schema For Derives

There are a couple of common attributes that need to be provided to most of the cynic derives:

  • schema_path - the file path to the schema you're working with.
  • query_module - the rust path to the module in which you.

You can provide these attributes manually on each of the derived structs you write. But if you've got a lot of structs this might be tedious and/or just a lot of noise.

To work around this, cynic provides the schema_for_derives macro. This macro can be applied to any mod and will populate the schema_path & query_module attributes of any cynic derives contained within.

For example this module contains a single QueryFragment and inherits it's schema_path & query_module parameters from that outer schema_for_derives attribute:


#![allow(unused)]
fn main() {
#[cynic::schema_for_derives(
    file = r#"schema.graphql"#,
    module = "schema",
)]
mod queries {
    use super::schema;

    #[derive(cynic::QueryFragment, Debug)]
    #[cynic(graphql_type = "Query")]
    pub struct PullRequestTitles {
        #[arguments(name = "cynic".into(), owner = "obmarg".into())]
        pub repository: Option<Repository>,
    }
}
}

Building Queries Manually

The previous section of the book showed how to use the various derive macros cynic provides to build out GraphQL queries. If you've got a more complex use case, these might not always provide the functionality you're looking for. Cynic provides lower level functionality for building queries without these derives, and that's what we'll go into in this chapter.

This chapter is for advanced users of cynic, or those who're curious about how it works under the hood. If you're new to cynic or want the easiest path to making queries you probably want to go back to the Deriving GraphQL Queries section.

The Query DSL

Writing Query Fragments

Upgrading Cynic v1 to v2

Cynic made a number of breaking changes in v2. I've tried to make the upgrade process as smooth as possible despite these, but here's a guide for how to do an upgrade.

Update your Cargo.toml

First update the version reference for cynic in your Cargo.toml to 2.

Some of the names of features changed in v2, so you may also need to update those:

old featurenew feature
surfhttp-surf
reqwesthttp-reqwest
reqwest-blockinghttp-reqwest-blocking

Cynic v1 had some features that existed only to enable surf features:

  • surf-h1-client
  • surf-curl-client
  • surf-wasm-client
  • surf-middleware-logger
  • surf-encoding.

These no longer exist in cynic v2 - you should remove these features and instead add the equivalents to the surf entry in your Cargo.toml

Update queries with variables

Any of your existing QueryFragments that take variables will need to be updated to the new argument syntax. This new syntax is very similar to the underlying GraphQL argument syntax. For example if you currently have

#[arguments(
    first = args.page_size,
    states = Some(vec![PullRequestState::Merged]),
    after = &args.pr_cursor
)]

You should change this to

#[arguments(first: $page_size, states = ["MERGED"], after: $pr_cursor)]

Note that cynic will no longer re-case arguments: you should use the names of arguments/input fields as they appear in the graphql schema.

Add Fallbacks to InlineFragments

If your queries have any InlineFragments without fallbacks you'll need to add one to them. Cynic v2 no longer does exhaustiveness checking on InlineFragments, so requires a fallback every time.

Update JSON decoding

If you are manually decoding cynic responses you should update the code responsible to use serde_json directly rather than Operation::decode_response.

Update ::build calls

The QueryBuilder::build, MutationBuilder::build and SubscriptionBuilder::build calls now take their parameter by value, so you should update any uses of them accordingly.

Update deprecations

Rust itself should let you know about the deprecations, but you will likely need to:

  • Use the QueryVariables derive instead of FragmentArguments.
  • Find any structs that derive QueryFragment and are using the argument_struct parameter. You need to rename this to variables.

Sending HTTP Request Manually

The cynic::http module provides integrations for some HTTP clients, but sometimes you might want to make a request manually: either because you're using a client that cynic doesn't support, or the provided integrations just aren't sufficient in some way.

It's simple to make an HTTP query manually with cynic:

  • cynic::Operation implements serde::Serialize to build the body of a GraphQL request. This can be used with whatever JSON encoding functionality your HTTP client provides.
  • The cynic::QueryFragment derive generates a serde::Deserialize impl that you can use to deserialize a cynic::GraphQlResponse<YourQueryFragment>

For instance, to make a request with the reqwest::blocking client:


#![allow(unused)]
fn main() {
use cynic::{QueryBuilder, GraphQlResponse};

let operation = AllFilmsQuery::build(());

let response = reqwest::blocking::Client::new()
    .post("https://swapi-graphql.netlify.app/.netlify/functions/index")
    .json(&operation)
    .send()
    .unwrap();

let all_films_result = response.json::<GraphQlResponse<AllFilmsQuery>>.unwrap();
}

Now you can do whatever you want with the result.