graphql_client, Rust types generated from your own query document
Typed, correct GraphQL requests and responses in Rust
At a glance
- What is it?
- graphql_client is a Rust library that reads a GraphQL query and the server schema at compile time and generates matching Rust types, so a response cannot disagree with the query that produced it. The interesting parts are the ones the compiler takes over from you, including deprecation warnings and custom scalar handling.
- Who is it for?
- graphql_client suits Rust teams calling a GraphQL API who want the response type checked against the query rather than hand-written, and it suits projects that share a schema across services, since the same schema file drives the generated types on every side.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 73 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The schema and the query are inputs to the compiler
A typed GraphQL client library for Rust is the one line description in the README, and the feature list underneath it is where the design shows. The first entry is precise types for query variables and responses. The second is support for GraphQL fragments, objects, unions, inputs, enums, custom scalars and input objects.
The mechanism is a procedural macro, and the README walks through it as a tutorial. You write your query in something like graphiql and save it in a .graphql file in your project. You download the schema, either with the CLI the project provides or with any other tool, because as the README says it does not matter what tool you use, the resulting schema.json is the same. Then you point the derive at both files:
use graphql_client::GraphQLQuery;
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "tests/unions/union_schema.graphql",
query_path = "tests/unions/union_query.graphql",
)]
pub struct UnionQuery;In order to provide precise types for a response, graphql_client needs to read the query and the schema at compile time. That sentence is the whole contract, and it has consequences in both directions. The types are correct because the compiler checks them against the actual schema, and the types are correct only as of the last time you compiled, because the schema file is what the macro reads.
The derive generates a module named after the struct in snake case, so UnionQuery produces union_query. That module holds all the struct and enum definitions needed to deserialize the response. The root type for the response is named ResponseData, and the GraphQL response takes the form of a Response<ResponseData>, where the Response type is always the same. The module also contains a struct called Variables, representing the variables the query expects.
The schema path accepts both the GraphQL schema language and JSON, which is a practical detail: a team that already has an introspected schema.json does not have to convert it.
Building the request body, and the shape of the response
Sending the query is where the generated types meet the network, and the README keeps the responsibility split clean. The GraphQLQuery trait is implemented for the struct under derive, so a complete query body can be created with a single call, described in the tutorial as the important line:
use graphql_client::{GraphQLQuery, Response};
use std::error::Error;
use reqwest;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "tests/unions/union_schema.graphql",
query_path = "tests/unions/union_query.graphql",
response_derives = "Debug",
)]
pub struct UnionQuery;
async fn perform_my_query(variables: union_query::Variables) -> Result<(), Box<dyn Error>> {
// this is the important line
let request_body = UnionQuery::build_query(variables);
let client = reqwest::Client::new();
let mut res = client.post("/graphql").json(&request_body).send().await?;
let response_body: Response<union_query::ResponseData> = res.json().await?;
println!("{:#?}", response_body);
Ok(())
}The library does not own the HTTP client here. reqwest::Client is created by you, the URL is yours, and the body is built by build_query and posted as JSON. The library's contribution is the typed payload and the typed deserialization target, so the only place you can get the types wrong is where you have not written any types at all.
The response side is a Response<union_query::ResponseData>, and because the Response wrapper is always the same, error handling is uniform across every query in your codebase rather than per query.
A complete example using the GitHub GraphQL API is available in the examples/github directory, and the workspace also carries examples/hasura/ and examples/web/, so there is a reference for a public API, for a database-backed GraphQL server, and for the browser case.
The compiler takes over the checks you would otherwise forget
Two features stand out because they move work from review to the compiler.
The first is deprecation handling. The generated code has support for the @deprecated field annotations, and you configure how deprecations are handled through the deprecated argument in the GraphQLQuery derive. The valid values are allow, where the response struct fields are not marked as deprecated, warn, where the response struct fields are marked with #[deprecated], and deny, where the struct field is not usable. That last option is the useful one for a large codebase, because it turns a server-side schema change into a compile error in every client that still reads the removed field.
The second is custom scalars. In GraphQL, five scalar types, Int, Float, String, Boolean and ID, are available out of the box and are automatically mapped to equivalent Rust types. A service provider can define additional custom scalars by adding a declaration like scalar URI to the server schema. If such a custom scalar is defined, the generated code references it, which means you have to provide a matching Rust type in the scope of the struct under derive. It can be as simple as a declaration like type URI = String, and that gives you freedom on how to treat custom scalars as long as they can be deserialized.
If those declarations are not provided, you get a build error, and the README shows it:
error[E0412]: cannot find type `URI` in module `super`
|
| #[derive(GraphQLQuery)]
| ^^^^^^^^^^^^ not found in `super`
|
= note: possible candidate is found in another module, you can import it into scope:
crate::repo_view::URIThe error is a normal Rust name resolution error, which is the point. The macro does not invent a mapping for your domain type, it requires you to state one, and the compiler holds you to it.
Two smaller options round out the ergonomics. The response_derives option lets you add arbitrary derives to the generated response types, since the generated code always derives serde::Deserialize but you may want to print them with Debug, compare them with PartialEq, or derive anything else. The skip_serializing_none option gives implicit null support: the generated code will skip the serialization of None values.
Two ways to produce the same code
There is a second workflow, and it exists because the derive macro is not always the right shape of tool.
The alternative is a command line interface. The project provides a CLI, and the README describes it as a way to introspect GraphQL APIs and generate a module from a command line interface to the library:
$ cargo install graphql_client_cli
$ graphql-client --helpThe key point is that the two routes are not different in output. The derive reads the query and schema at compile time, and the CLI generates the same module ahead of time, so the result is the same code either way. What differs is when the work happens and what you can do with it.
The derive suits a project where the schema and query live in the same repository as the client and change together. The compile-time reading means the types update as part of the build, and there is no generated file to commit and no stale copy to reconcile.
The CLI suits the cases the derive cannot cover. If the schema lives in another repository, or is produced by a service discovery step, or you want to generate code for a query that a client does not own, generating ahead of time is the only option. It also fits a build pipeline where a schema fetch runs in CI, since the generated module can be checked in or consumed as an artefact. The cost of that route is that a change to the schema now needs a regeneration step, and if the generation step is not run, the types are quietly wrong rather than loudly wrong.
The README is explicit that it does not matter what tool you use to download the schema, only that the resulting schema.json is the same, which tells you the schema file is the interface between the two workflows rather than the code generator.
What the workspace contains, and what to check before you commit
The repository is a Cargo workspace with resolver version 2, and the members list shows how the pieces are separated: graphql_client for the library, graphql_client_cli for the command line tool, graphql_client_codegen for the code generation, graphql_query_derive for the procedural macro, and graphql-introspection-query for the introspection query document. The example crates are pulled in with a wildcard entry for examples/*, which is why examples/github/, examples/hasura/ and examples/web/ are all built by the same cargo commands.
That split is worth noting because it means the macro and the code generator are separate crates. The derive is a thin front end over the same generator the CLI uses, and the two can evolve in step because they share the implementation rather than duplicating it.
The version numbers matter. The workspace package version is 0.16.0, and rust-version is 1.66.0, which is a fairly old minimum and a sign that the library does not expect a recent toolchain. The release history shows 0.16.0 from 2026-01-15 and 0.15.0 from 2025-12-08, with an earlier 0.8.0 from 2019 that was the release that moved to the 2018 edition and added the web feature in graphql_client. The project is not archived and the last push was on 2026-08-11, so work is continuing.
The licence is dual, with LICENSE-APACHE and LICENSE-MIT files at the root, and there is a CODE_OF_CONDUCT.md and a CONTRIBUTING.md alongside a CHANGELOG.md.
Before committing to this approach, two checks are worth doing. The first is that the schema file your build reads is the schema your server serves, because a stale schema produces types that compile and are wrong, and the compiler cannot help you there. The second is to decide where the query documents live. If they live in the client repository and are edited by hand, the schema and the query can drift from each other in ways the type system will not catch, because the macro checks each file against the other but never asks whether the query is the one your frontend sends. The web and hasura examples are the reference points for how that is supposed to be arranged.
Editorial conclusion
graphql_client suits Rust teams calling a GraphQL API who want the response type checked against the query rather than hand-written, and it suits projects that share a schema across services, since the same schema file drives the generated types on every side. It is the wrong choice if you want runtime flexibility over compile-time guarantees, because a query change requires a rebuild, and it is the wrong choice if your API is not GraphQL, since the whole approach assumes a query document and a schema exist. Before adopting it, confirm the schema you generate is the schema your server actually serves, because the macro reads the file at compile time and will happily type-check against a stale copy, and decide early whether you will use the derive macro or the CLI, since they produce the same modules from the same inputs.
Frequently asked questions
How do I use graphql_client in a Rust project?
Save your query in a .graphql file, download the schema as schema.json using the provided CLI or any other tool, then derive GraphQLQuery with schema_path and query_path pointing at both files. The derive generates a module named after the struct in snake case containing ResponseData, Variables and the response types.
What does the GraphQLQuery derive generate?
It generates a module with all the struct and enum definitions needed to deserialize the response. The root type is named ResponseData, the response takes the form of a Response<ResponseData> where the Response type is always the same, and a Variables struct represents the variables the query expects.
How do I handle custom GraphQL scalars in graphql_client?
You provide a matching Rust type in the scope of the struct under derive, for example a declaration like type URI = String. If you do not, the build fails with a cannot find type error pointing at the derive.
What is the difference between the derive macro and the graphql-client CLI?
The derive reads the query and schema at compile time, while the CLI introspects the API and generates the module ahead of time. The README notes the output is the same either way, and that it does not matter what tool is used to download the schema since the resulting schema.json is identical.
Can graphql_client mark deprecated schema fields?
Yes. The deprecated argument in the derive takes allow, where fields are not marked, warn, where fields get a #[deprecated] attribute, or deny, where the field is not usable. The generated code supports the @deprecated field annotations from the schema.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/graphql-rust-graphql-client)Was this useful?