Cynic
A bring your own types GraphQL client for Rust
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
. (thoughgraphql-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:
- The guide that you're reading on cynic-rs.dev
- 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.
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:
- An existing rust project (though you can just run
cargo new
if you don't have one). - A GraphQL API that you'd like to query.
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 = "3", 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'll also want to add a [build-dependencies]
:
[build-dependencies]
cynic-codegen = { version = "3" }
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.
Fetching the GraphQL schema
Each GraphQL server has a schema that describes the information it can provide.
Cynic needs a local copy of this schema for any APIs that it needs to talk to.
We'll fetch this schema using cynic-cli
. Run the following in the root of
your project:
# First, install cynic-cli if you don't already have it:
cargo install --locked cynic-cli
# Next, we'll introspect the schema.
# I'm using the StarWars schema in this guide but you can use any API you want
cynic introspect https://swapi-graphql.netlify.app/.netlify/functions/index -o schemas/starwars.graphql
Adding your schema to the build.
GraphQL APIs have a schema that defines the data they can provide. Cynic needs
a copy of this schema to make sure you're writing valid queries. To register
your schema you should create a build.rs
in the root of your project (next to
Cargo.toml
) and add the following:
fn main() { cynic_codegen::register_schema("starwars") .from_sdl_file("schemas/starwars.graphql") .unwrap() .as_default() .unwrap(); }
I'm using the Star Wars schema which I've put in a schemas folder, but you should adjust the path and name for whatever schema you're using.
Defining your schema module.
Cynics macros need a "schema module" - this module contain a bunch of autogenerated types that cynic do its job. To define one of these, you should open the rust file where you want to write a query and add the following:
#![allow(unused)] fn main() { // Pull in the Star Wars schema we registered in build.rs #[cynic::schema("starwars")] mod schema {} }
I registered my schema as starwars
in build.rs so I've passed that name in
here. You should use whatever name you decided on in your build.rs
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.
For example, I've chosen to add the star wars schema and the following query:
query FilmDirectorQuery(id: ID!){
film {
title
director
}
}
and been given, the following rust code:
#![allow(unused)] fn main() { #[derive(cynic::QueryFragment, Debug)] struct Film { title: Option<String>, director: Option<String>, } #[derive(cynic::QueryVariables)] struct FilmArguments { id: Option<cynic::Id>, } #[derive(cynic::QueryFragment, Debug)] #[cynic(graphql_type = "Root", variables = "FilmArguments")] struct FilmDirectorQuery { #[arguments(id: $id)] film: Option<Film>, } }
You should add this to the same file as your schema module.
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.
Schemas
GraphQL APIs have a schema that defines the data they can provide. Cynic uses this schema to verify that the queries & mutations you're writing are valid and typesafe.
The rest of this page assumes you have a copy of the schema for the remote API
checked in to your repository. If that's not the case, see [Introspecting an
API][1].
Pre-registering Schemas
Most of cynics macros need access to a copy of the GraphQL schema. The easiest
way to provide this is to register the schema in your build.rs
, with some
code similar to this:
fn main() { cynic_codegen::register_schema("github") .from_sdl_file("schemas/github.graphql") .unwrap() .as_default() .unwrap(); }
This will register a schema called github
as well as registering it as the
default schema. This schema will now automatically be used by any cynic derive
in your crate.
Schema Modules
The cynic derives also require a "schema module" to be in scope. If you've already pre-registered your schema then defining this looks like:
#[cynic::schema("starwars")
mod schema {}
Working with Multiple schemas
If you need to work with multiple APIs with different schemas, simply give them different names when registering. Here we add the starwars schema as well:
#![allow(unused)] fn main() { cynic_codegen::register_schema("starwars") .from_sdl_file("schemas/starwars.graphql") .unwrap(); }
Note how we didn't call .as_default()
here - the github
schema remains our
default, but we can also use starwars
when we need to. To use this in a
derive you'll have to provide the schema
argument:
#[derive(cynic::QueryFragment, Debug)]
#[cynic(schema = "starwars")]
struct Film {
title: Option<String>,
director: Option<String>,
}
You'll also need to make sure you define a schema module for each of your
schemas, and make it available as schema
in the files where you're defining
your queries.
Introspecting an API
Each GraphQL server has a schema that describes the information it can provide. Cynic needs a local copy of the schema for any APIs that it needs to talk to.
This page documents various ways to get a copy of that schema and keep it up to date.
Introspecting with the cynic
CLI
The easiest way to introspect a schema is with the cynic
CLI.
You can install the CLI with cargo
:
cargo install --locked cynic-cli
The CLI has an introspect
subcommand that can be used to introspect a server
and output it's schema to a file. The format for this command is:
cynic introspect [GRAPHQL_URL] -o [OUTPUT_FILE]
To fetch the StarWars API schema we use in the documentation and
put it in `schemas/starwars.graphql`:
```sh
cynic introspect https://swapi-graphql.netlify.app/.netlify/functions/index -o schemas/starwars.graphql
```
Providing Headers
Some GraphQL APIs require headers to introspect (e.g. for authentication). The
introspection command supports these via the -H
parameter, which expects a
header in HTTP format.
The GitHub API sometimes requires an `Authorization` header which can be provided with `-H`:
```sh
cynic introspect -H "Authorization: Bearer [GITHUB_TOKEN]" "https://graphql.org/swapi-graphql" -o schemas/github.graphql
```
Keeping the schema up to date
When using cynic, we recommend you keep a copy of the remote schema checked into your repository. This means you can always build your application from source without having to download the schema.
Some schemas might not be updated particularly often, so you might run introspection once when starting your project and be done.
But if you want to keep your local copy of the schema up to date, I'd recommend adding a period CI job that will fetch the schema and make a commit with the up to date schema, running your normal CI process to make sure there were no breakages.
If you're using GitHub Actions you could use the following as a template:
name: Update local copy of GraphQL schema
on:
schedule:
- cron: 0 9 * * *
jobs:
update-schema:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v3
- name: Run introspection
uses: obmarg/action-graphql-introspect@main
with:
server-url: https://swapi-graphql.netlify.app/.netlify/functions/index
output-file: schemas/starwars.graphql
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
commit-message: "Update StarWars GraphQL Schema"
branch: graphql-schema-updates
title: "Update StarWars GraphQL Schema"
message: "This is an automated pull request to update our local schema cache"
This action requires that you've given GitHub actions permission to open a PR. See [the create-pull-request action documentation for more details][1]
Introspecting in Code
You may also want to run an introspection query in code, either to integrate
with cynic or if you just need an introspection query for other reasons. The
cynic-introspection
crate can do this for you, please refer to its
documentation.
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)] 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)] 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(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(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:
GraphQL | Cynic |
---|---|
input: { filters: "ACTIVE" } | input: { filters: "ACTIVE" } |
values: ["Hello"] | values: ["Hello"] |
values: ["Hello"] | values: ["Hello"] |
arg1: "Foo", arg2: "Bar" | arg1: "Foo", arg2: "Bar" |
arg1: null | arg1: 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( 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
QueryFragment
s 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.variables
defines theQueryVariables
struct that is available toarguments
attributes on fields of the given struct.schema
tells cynic which schema to use to validate your InlineFragments. The schema you provide should have been registered in yourbuild.rs
. This is optional if you're using the schema that was registered as default, or if you're usingschema_path
instead.schema_path
sets a path to the GraphQL schema. This is only required if you're using a schema that wasn't registered inbuild.rs
.schema_module
tells cynic where to find your schema module. This is optional and should only be needed if your schema module is not in scope or namedschema
.
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 toOption<Vec<Option<i32>>
. This isn't a very nice type to work with - applying theflatten
attribute lets you represent this as aVec<i32>
in your QueryFragment. Any outer nulls become an empty list and inner nulls are dropped. - The
spread
attr can be used to spread anotherQueryFragment
s into the currentQueryFragment
, if each of theQueryFragment
s point at the same GraphQL type. - The
feature
attribute can be used to feature flag parts of a query, allowing cynic to support different versions of a schema with the sameQueryFragments
. See feature flagging queries for more details.
Related
- QueryVariables are used to provide variables to a QueryFragment.
- Recursive queries are supported by QueryFragments.
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. 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.
Exhaustiveness Checking
By default, cynic checks the exhuastiveness of Enum
s - you should provide a
variant for each enum value in the GraphQL schema. You can also provide a fallback
variant to provide forwards compatability - if the server adds new enum values they'll be caught by this variant.
You can opt-out of this exhaustiveness using the #[cynic(non_exhaustive)]
attribute. When this is present exhaustiveness is not checked, and the
fallback variant is used for all the variants missing from the selection.
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 toSCREAMING_SNAKE_CASE
to be consistent with GraphQL conventions.schema
tells cynic which schema to use to validate this Enum. The schema you provide should have been registered in yourbuild.rs
. This is optional if you're using the schema that was registered as default, or if you're usingschema_path
instead.schema_path
provides a path to some GraphQL schema SDL. This is only required if you're using a schema that wasn't registered inbuild.rs
.schema_module
tells cynic where to find your schema module. This is optional and should only be needed if your schema module is not in scope or is named something other thanschema
.non_exhaustive
can be provided to mark an enum as non-exhaustive. Such enums are required to have a fallback variant, but not required to have a variant for each value in the schema.
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.- The
fallback
attribute can be provided on a single variant. This variant will be used when we receive a value we didn't expect from the server - such as when the server has added a new variant since we last pulled its schema. This variant can optionally have a single string field, which will receive the value we received from the server.
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 beString
fields in Rust.Int
fields in GraphQL should bei32
in Rust.Boolean
fields in GraphQL map tobool
in Rust.ID
fields in GraphQL map to thecynic::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() { use chrono::{DateTime, Utc}; impl_scalar!(DateTime<Utc>, schema::DateTime); }
You can now use a DateTime<Utc>
type for any DateTime
in your scheam.
This `impl_scalar` call should be placed in the crate that defines the the
`schema` module.
#[derive(Scalar)]
You can also derive Scalar
on any newtype structs:
#![allow(unused)] fn main() { #[derive(cynic::Scalar, serde::Serialize)] 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.
This derive only works on newtype structs - for any more complex datatype
you'll have to implement cynic::Scalar yourself, or use `impl_scalar` above
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 your schema module. This is optional and should only be needed if your schema module is not in scope or namedschema
.
Variables
GraphQL queries can declare variables that can be passed in, allowing you to set the values of arguments without specifying those values directly in your query. You can use this to pass values from the rest of your program into cynic.
You can declare a set of variables by making a struct and deriving
QueryVariables
on it:
#![allow(unused)] fn main() { #[derive(cynic::QueryVariables)] struct FilmVariables { id: Option<cynic::Id>, } }
The struct above declares a single variable named id
of Option<cynic::Id>
(or ID
in GraphQL terms).
Using QueryVariables
To use variables in a query you need to tell cynic which QueryVariables
struct is in scope. You do this by providing a a variables
parameter to the
QueryFragment
derive. This allows you to provide any of the variables in your
QueryVariables
struct to any arguments in this fragment. For example:
#![allow(unused)] fn main() { #[derive(cynic::QueryFragment, Debug)] #[cynic( 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
.
Struct Attributes
QueryVariables can be configured via attributes at the struct level:
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.
Field Attributes
Each field can also have 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 anOption
field will be sent asnull
to servers, but if you provideskip_serializing_if="Option::is_none"
then the field will not be provided at all.
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 InputObject
s. 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
tells cynic which schema to use to validate your InlineFragments. The schema you provide should have been registered in yourbuild.rs
. This is optional if you're using the schema that was registered as default, or if you're usingschema_path
instead.schema_path
sets a path to the GraphQL schema. This is only required if you're using a schema that wasn't registered inbuild.rs
.schema_module
tells cynic where to find your schema module. This is optional and should only be needed if your schema module is not in scope or namedschema
.
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 anOption
field will be sent asnull
to servers, but if you provideskip_serializing_if="Option::is_none"
then the field will not be provided at all.
Inline Fragments
GraphQL provides interfaces & union types, which are abstract types that can
resolve to one of several concrete objects types. To query these cynic provides
the InlineFragments
derive.
InlineFragments
can be derived on an enum with one variant for each sub-type
that you're interested in querying. Each of the variants should have a single
field containing a type that implements QueryFragment
for one of the
sub-types.
For example, the GitHub API has an Assignee
union type which could be queried
with:
#![allow(unused)] fn main() { #[derive(cynic::InlineFragments)] 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 their 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)] 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)] pub enum Actor { User(User), #[cynic(fallback)] Other(ActorFallback), } #[derive(cynic::QueryFragment)] 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)] enum Assignee { Bot(Bot), User(User) #[cynic(fallback)] Other(String) } }
This functionality is currently only available for unions.
Exhaustiveness Checking
By default, cynic doesn't implement any kind of exhuastiveness checking on
InlineFragments
. This is in line with standard GraphQL behaviour: it's not a
bug to make a query that skips some types that could be returned.
But some users may want exhaustivness checking. For union types this can be
enabled with the exhaustive
attribute:
#![allow(unused)] fn main() { #[derive(cynic::InlineFragments)] #[cynic(exhaustive)] enum Assignee { Bot(Bot), User(User) #[cynic(fallback)] Other(String) } }
If the a new type is to this union then cynic will fail to compile.
This check uses the name of the variants to perform its checks, which is not
fool proof. There's no guarantee that the `Bot` type actually queries for the
`Bot` type in the server. So be careful when using this.
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
tells cynic which schema to use to validate your InlineFragments. The schema you provide should have been registered in yourbuild.rs
. This is optional if you're using the schema that was registered as default, or if you're usingschema_path
instead.schema_path
sets a path to the GraphQL schema. This is only required if you're using a schema that wasn't registered inbuild.rs
.schema_module
tells cynic where to find your schema module. This is optional and should only be needed if your schema module is not in scope or namedschema
.exhaustive
adds exhaustiveness checking to anInlineFragment
. Note that this is only supported on GraphQL unions currently (though I would accept a PR to add it to interfaces)
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 aQueryFragment
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 aBox
, 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 theOption
.
Some examples of the changes:
GraphQL Type | Rust Type Without Recurse | Rust Type With Recurse |
---|---|---|
T | Option<T> | Box<Option<T>> |
T! | T | Option<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>> |
Working with Large APIs
Some APIs have fairly large schemas, and this introduces some performance challenges for cynic. Runtime performance should be unaffected, but it can lead to extended compile times and make rust-analyzer less responsive than it would otherwise be.
There's several tricks to help with this though:
Registering Schemas with rkyv
If you're not already you should be pre-registering your schema.
You should also enable the rkyv
feature flag in cynic_codegen
. This allows
the pre-registration to store schemas in an optimised format, which avoids a
lot of comparatively slow parsing.
Splitting Crates
Definitely consider moving your schema module into a separate crate. These modules contain a lot of generated code and are quite expensive to compile. Moving them to a separate crate should reduce the chance that unrelated changes cause it to recompile. Note that you'll need to register your schema in both the schema module crate and any crate where you use the cynic derives.
You can also consider moving your query structs into their own crate, for reasons similar to the above. Though it may be worth testing whether this actually helps - with rkyv turned on these shouldn't be too slow. But it really depends on how many of them you have.
Example Workspace Setup
All subheadings are clickable and lead to executable Rust crates that follow the corresponding snippets.
$ tree . -I target -I Cargo.lock
. <-- workspace root
├── Cargo.toml
├── README.md
├── api <-- uses query structs
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── query <-- define query structs and check against schema
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── schema <-- generate schema structs
├── Cargo.toml
├── build.rs
└── src
└── lib.rs
Export schema generated by #[cynic::schema(...)]
cynic-codegen
is used in the build.rs
to register the schema.
Make sure to enable the rkyv
feature flag for faster compilation!
/// Register github schema for creating definitions
fn main() {
cynic_codegen::register_schema("github")
.from_sdl_file("../../schemas/github.graphql")
.expect("Failed to find GraphQL Schema");
}
The next step is to generate and export the large GitHub GraphQL Schema from lib.rs
as a reusable module.
The module can be named however you like at this point, but either schema
or an identifier related to the registered schema is recommended.
/// Cache and export large github schema, as it is expensive to recreate.
#[cynic::schema("github")]
pub mod github { }
Deriving queries with #[derive(cynic::QueryFragment)]
Queries require metadata generated by cynic::register_schema
, so the same incantation used in schema/build.rs
must be repeated for query/build.rs
.
/// Register github schema for creating structs for queries
fn main() {
cynic_codegen::register_schema("github")
.from_sdl_file("../../schemas/github.graphql")
.expect("Failed to find GraphQL Schema");
}
Next, create a lib.rs
file within the query
crate that begins with the following lines.
use cynic; // Import for derive macros
use schema::github as schema; // Rename is vital! Must import as schema!
As indicated by the second comment, when importing the codegenned schema from its crate, its module must be named schema
, otherwise the upcoming #[derive(cynic::QueryFragment)]
macro applications will fail.
The remainder of this file shall contain exportable structs as output by cynic querygen
.
Sending GraphQL queries
Now that both the schema and queries are cached as separate crates, an application can make use of these by using the query
crate to run queries.
/// Safely run queries using generated query objects,
/// which have been checked against the underlying schema.
/// We do not need to codegen again.
use query::*;
fn main() {
let result = run_query();
println!("{:#?}", result);
}
fn run_query() -> cynic::GraphQlResponse<PullRequestTitles> {
...
}
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 feature | new feature |
---|---|
surf | http-surf |
reqwest | http-reqwest |
reqwest-blocking | http-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 ofFragmentArguments
. - Find any structs that derive
QueryFragment
and are using theargument_struct
parameter. You need to rename this tovariables
.
Upgrading Cynic v2 to v3
Cynic made a few minor breaking changes in v3, but they shouldn't affect most users. However, there were a bunch of quality of life changes in this version. This guide will show you how to take advantage of these.
Update your Cargo.toml
First update the version reference for cynic in your Cargo.toml to 3
.
This verison also added an optional rkyv
feature that can speed up
compilation for some users. Add this in if you like.
Pre-register your schemas
v3 added the concept of pre-registering schemas. This saves you from having to
pass the schema_path
attribute to the derives, and makes the
schema_for_derives
macro mostly redundant (assuming you're only using one
schema at least).
To do this you should create a build.rs
in the root of your project, with
the following contents:
fn main() { cynic_codegen::register_schema("github") .from_sdl_file("schemas/github.graphql") .unwrap() .as_default() .unwrap(); }
Where github
is the name of your schema and schemas/github.graphql
is the
path to the schema SDL (relative to the build.rs
file).
See the schemas page for more details.
After you've done this, you should be able to remove any schema_path
attributes, and any uses of the schema_for_derives
attribute.
Replace use_schema
with #[cynic::schema]
If you've pre-registered your schema you should update the code where you call
use_schema
. Find this code, it'll look something like this:
#![allow(unused)] fn main() { mod schema { cynic::use_schema!("github.graphql") } }
and replace it with
#![allow(unused)] fn main() { #[cynic::schema("github")] mod schema {} }
where github
is the name you gave the schema in your build.rs
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
implementsserde::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 aserde::Deserialize
impl that you can use to deserialize acynic::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.
Advanced Use Cases
This section details some of the more advanced features of cynic. Most users probably won't need these, but they're there if you do.
Feature Flagged Queries
Often in GraphQL there's only one server for a given schema. But sometimes there might be many servers that serve a different schema, and those servers might be serving different versions of that same schema. A classic example of this is the introspection query: servers support a different set of fields depending on which version of the specification they support and/or which RFCs they have implemented on top of that specification.
To support these cases, cynic allows you to associate fields in a
QueryFragment
with a feature:
#![allow(unused)] fn main() { #[derive(cynic::QueryFragment, Debug)] struct AuthorQuery { __typename: String, #[cynic(feature = "shiny")] shiny_new_field: Option<String> } }
This Author
struct will only query for shiny_new_field
if the shiny
feature has been enabled.
To enable features you need to use the lower-level OperationBuilder
to build
your Operation
(rather than the QueryBuilder
that is usually recommended):
#![allow(unused)] fn main() { let operation = cynic::OperationBuilder::<QueryWithFeatures, ()>::query() .with_variables(()) .with_feature_enabled("shiny") .build()?; }
For examples of this feature in action, take a look in the
cynic-introspection
crate, which uses it to support multiple versions of the
GraphQL specification, and the various RFCs that servers support.