Juniper

Juniper is a GraphQL server library for Rust. Build type-safe and fast API servers with minimal boilerplate and configuration.

GraphQL is a data query language developed by Facebook intended to serve mobile and web application frontends.

Juniper makes it possible to write GraphQL servers in Rust that are type-safe and blazingly fast. We also try to make declaring and resolving GraphQL schemas as convenient as possible as Rust will allow.

Juniper does not include a web server - instead it provides building blocks to make integration with existing servers straightforward. It optionally provides a pre-built integration for the Hyper, Iron, Rocket, and Warp frameworks, including embedded Graphiql for easy debugging.

Features

Juniper supports the full GraphQL query language according to the specification (October 2021), including interfaces, unions, schema introspection, and validations. It does not, however, support the schema language.

As an exception to other GraphQL libraries for other languages, Juniper builds non-null types by default. A field of type Vec<Episode> will be converted into [Episode!]!. The corresponding Rust type for e.g. [Episode] would be Option<Vec<Option<Episode>>>.

Integrations

Data types

Juniper has automatic integration with some very common Rust crates to make building schemas a breeze. The types from these crates will be usable in your Schemas automatically.

Web Frameworks

API Stability

Juniper has not reached 1.0 yet, thus some API instability should be expected.

Quickstart

This page will give you a short introduction to the concepts in Juniper.

Juniper follows a code-first approach to defining GraphQL schemas. If you would like to use a schema-first approach instead, consider juniper-from-schema for generating code from a schema file.

Installation

[dependencies]
juniper = "0.16.0"

Schema example

Exposing simple enums and structs as GraphQL is just a matter of adding a custom derive attribute to them. Juniper includes support for basic Rust types that naturally map to GraphQL features, such as Option<T>, Vec<T>, Box<T>, String, f64, and i32, references, and slices.

For more advanced mappings, Juniper provides multiple macros to map your Rust types to a GraphQL schema. The most important one is the graphql_object procedural macro that is used for declaring an object with resolvers, which you will use for the Query and Mutation roots.

#![allow(unused_variables)]
extern crate juniper;
use std::fmt::Display;
use juniper::{
    graphql_object, EmptySubscription, FieldResult, GraphQLEnum, 
    GraphQLInputObject, GraphQLObject, ScalarValue,
};

struct DatabasePool;
impl DatabasePool {
    fn get_connection(&self) -> FieldResult<DatabasePool> { Ok(DatabasePool) }
    fn find_human(&self, _id: &str) -> FieldResult<Human> { Err("")? }
    fn insert_human(&self, _human: &NewHuman) -> FieldResult<Human> { Err("")? }
}

#[derive(GraphQLEnum)]
enum Episode {
    NewHope,
    Empire,
    Jedi,
}

#[derive(GraphQLObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct Human {
    id: String,
    name: String,
    appears_in: Vec<Episode>,
    home_planet: String,
}

// There is also a custom derive for mapping GraphQL input objects.

#[derive(GraphQLInputObject)]
#[graphql(description = "A humanoid creature in the Star Wars universe")]
struct NewHuman {
    name: String,
    appears_in: Vec<Episode>,
    home_planet: String,
}

// Now, we create our root Query and Mutation types with resolvers by using the
// object macro.
// Objects can have contexts that allow accessing shared state like a database
// pool.

struct Context {
    // Use your real database pool here.
    pool: DatabasePool,
}

// To make our context usable by Juniper, we have to implement a marker trait.
impl juniper::Context for Context {}

struct Query;

#[graphql_object(
    // Here we specify the context type for the object.
    // We need to do this in every type that
    // needs access to the context.
    context = Context,
)]
impl Query {
    fn apiVersion() -> &'static str {
        "1.0"
    }

    // Arguments to resolvers can either be simple types or input objects.
    // To gain access to the context, we specify a argument
    // that is a reference to the Context type.
    // Juniper automatically injects the correct context here.
    fn human(context: &Context, id: String) -> FieldResult<Human> {
        // Get a db connection.
        let connection = context.pool.get_connection()?;
        // Execute a db query.
        // Note the use of `?` to propagate errors.
        let human = connection.find_human(&id)?;
        // Return the result.
        Ok(human)
    }
}

// Now, we do the same for our Mutation type.

struct Mutation;

#[graphql_object(
    context = Context,
    // If we need to use `ScalarValue` parametrization explicitly somewhere
    // in the object definition (like here in `FieldResult`), we could
    // declare an explicit type parameter for that, and specify it.
    scalar = S: ScalarValue + Display,
)]
impl Mutation {
    fn createHuman<S: ScalarValue + Display>(context: &Context, new_human: NewHuman) -> FieldResult<Human, S> {
        let db = context.pool.get_connection().map_err(|e| e.map_scalar_value())?;
        let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?;
        Ok(human)
    }
}

// A root schema consists of a query, a mutation, and a subscription.
// Request queries can be executed against a RootNode.
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;

fn main() {
  let _ = Schema::new(Query, Mutation, EmptySubscription::new());
}

We now have a very simple but functional schema for a GraphQL server!

To actually serve the schema, see the guides for our various server integrations.

Juniper is a library that can be used in many contexts--it does not require a server and it does not have a dependency on a particular transport or serialization format. You can invoke the executor directly to get a result for a query:

Executor

You can invoke juniper::execute directly to run a GraphQL query:

// Only needed due to 2018 edition because the macro is not accessible.
#[macro_use] extern crate juniper;
use juniper::{
    graphql_object, EmptyMutation, EmptySubscription, FieldResult, 
    GraphQLEnum, Variables, graphql_value,
};

#[derive(GraphQLEnum, Clone, Copy)]
enum Episode {
    NewHope,
    Empire,
    Jedi,
}

// Arbitrary context data.
struct Ctx(Episode);

impl juniper::Context for Ctx {}

struct Query;

#[graphql_object(context = Ctx)]
impl Query {
    fn favoriteEpisode(context: &Ctx) -> FieldResult<Episode> {
        Ok(context.0)
    }
}

// A root schema consists of a query, a mutation, and a subscription.
// Request queries can be executed against a RootNode.
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;

fn main() {
    // Create a context object.
    let ctx = Ctx(Episode::NewHope);

    // Run the executor.
    let (res, _errors) = juniper::execute_sync(
        "query { favoriteEpisode }",
        None,
        &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
        &Variables::new(),
        &ctx,
    ).unwrap();

    // Ensure the value matches.
    assert_eq!(
        res,
        graphql_value!({
            "favoriteEpisode": "NEW_HOPE",
        })
    );
}

Type System

Most of the work in working with juniper consists of mapping the GraphQL type system to the Rust types your application uses.

Juniper provides some convenient abstractions that try to make this process as painless as possible.

Find out more in the individual chapters below.

Defining objects

While any type in Rust can be exposed as a GraphQL object, the most common one is a struct.

There are two ways to create a GraphQL object in Juniper. If you've got a simple struct you want to expose, the easiest way is to use the custom derive attribute. The other way is described in the Complex fields chapter.

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

fn main() {}

This will create a GraphQL object type called Person, with two fields: name of type String!, and age of type Int!. Because of Rust's type system, everything is exported as non-null by default. If you need a nullable field, you can use Option<T>.

We should take advantage of the fact that GraphQL is self-documenting and add descriptions to the type and fields. Juniper will automatically use associated doc comments as GraphQL descriptions:

!FILENAME GraphQL descriptions via Rust doc comments

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
/// Information about a person
struct Person {
    /// The person's full name, including both first and last names
    name: String,
    /// The person's age in years, rounded down
    age: i32,
}

fn main() {}

Objects and fields without doc comments can instead set a description via the graphql attribute. The following example is equivalent to the above:

!FILENAME GraphQL descriptions via attribute

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
#[graphql(description = "Information about a person")]
struct Person {
    #[graphql(description = "The person's full name, including both first and last names")]
    name: String,
    #[graphql(description = "The person's age in years, rounded down")]
    age: i32,
}

fn main() {}

Descriptions set via the graphql attribute take precedence over Rust doc comments. This enables internal Rust documentation and external GraphQL documentation to differ:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
#[graphql(description = "This description shows up in GraphQL")]
/// This description shows up in RustDoc
struct Person {
    #[graphql(description = "This description shows up in GraphQL")]
    /// This description shows up in RustDoc
    name: String,
    /// This description shows up in both RustDoc and GraphQL
    age: i32,
}

fn main() {}

Relationships

You can only use the custom derive attribute under these circumstances:

  • The annotated type is a struct,
  • Every struct field is either
    • A primitive type (i32, f64, bool, String, juniper::ID), or
    • A valid custom GraphQL type, e.g. another struct marked with this attribute, or
    • A container/reference containing any of the above, e.g. Vec<T>, Box<T>, Option<T>

Let's see what that means for building relationships between objects:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

#[derive(GraphQLObject)]
struct House {
    address: Option<String>, // Converted into String (nullable)
    inhabitants: Vec<Person>, // Converted into [Person!]!
}

fn main() {}

Because Person is a valid GraphQL type, you can have a Vec<Person> in a struct and it'll be automatically converted into a list of non-nullable Person objects.

Renaming fields

By default, struct fields are converted from Rust's standard snake_case naming convention into GraphQL's camelCase convention:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    first_name: String, // Would be exposed as firstName in the GraphQL schema
    last_name: String, // Exposed as lastName
}

fn main() {}

You can override the name by using the graphql attribute on individual struct fields:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
    #[graphql(name = "websiteURL")]
    website_url: Option<String>, // now exposed as `websiteURL` in the schema
}

fn main() {}

Or provide a different renaming policy on a struct for all its fields:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
#[graphql(rename_all = "none")] // disables any renaming
struct Person {
    name: String,
    age: i32,
    website_url: Option<String>, // now exposed as `website_url` in the schema
}

fn main() {}

Deprecating fields

To deprecate a field, you specify a deprecation reason using the graphql attribute:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
    #[graphql(deprecated = "Please use the name field instead")]
    first_name: String,
}

fn main() {}

The name, description, and deprecation arguments can of course be combined. Some restrictions from the GraphQL spec still applies though; you can only deprecate object fields and enum values.

Ignoring fields

By default, all fields in a GraphQLObject are included in the generated GraphQL type. To prevent including a specific field, annotate the field with #[graphql(ignore)]:

extern crate juniper;
use juniper::GraphQLObject;
#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
    #[graphql(ignore)]
    #[allow(dead_code)]
    password_hash: String, // cannot be queried or modified from GraphQL
}

fn main() {}

Complex fields

If you've got a struct that can't be mapped directly to GraphQL, that contains computed fields or circular structures, you have to use a more powerful tool: the #[graphql_object] procedural macro. This macro lets you define GraphQL object fields in a Rust impl block for a type. Note, that GraphQL fields are defined in this impl block by default. If you want to define normal methods on the struct, you have to do so either in a separate "normal" impl block, or mark them with #[graphql(ignore)] attribute to be omitted by the macro. Continuing with the example from the last chapter, this is how you would define Person using the macro:

#![allow(dead_code)]
extern crate juniper;
use juniper::graphql_object;

struct Person {
    name: String,
    age: i32,
}

#[graphql_object]
impl Person {
    fn name(&self) -> &str {
        self.name.as_str()
    }

    fn age(&self) -> i32 {
        self.age
    }

    #[graphql(ignore)]
    pub fn hidden_from_graphql(&self) {
        // [...]
    }
}

impl Person {
    pub fn hidden_from_graphql2(&self) {
        // [...]
    }
}

fn main() { }

While this is a bit more verbose, it lets you write any kind of function in the field resolver. With this syntax, fields can also take arguments:

extern crate juniper;
use juniper::{graphql_object, GraphQLObject};

#[derive(GraphQLObject)]
struct Person {
    name: String,
    age: i32,
}

struct House {
    inhabitants: Vec<Person>,
}

#[graphql_object]
impl House {
    // Creates the field `inhabitantWithName(name)`, returning a nullable `Person`.
    fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
        self.inhabitants.iter().find(|p| p.name == name)
    }
}

fn main() {}

To access global data such as database connections or authentication information, a context is used. To learn more about this, see the next chapter: Using contexts.

Description, renaming, and deprecation

Like with the derive attribute, field names will be converted from snake_case to camelCase. If you need to override the conversion, you can simply rename the field. Also, the type name can be changed with an alias:

extern crate juniper;
use juniper::graphql_object;

struct Person;

/// Doc comments are used as descriptions for GraphQL.
#[graphql_object(
    // With this attribute you can change the public GraphQL name of the type.
    name = "PersonObject",

    // You can also specify a description here, which will overwrite 
    // a doc comment description.
    description = "...",
)]
impl Person {
    /// A doc comment on the field will also be used for GraphQL.
    #[graphql(
        // Or provide a description here.
        description = "...",
    )]
    fn doc_comment(&self) -> &str {
        ""
    }

    // Fields can also be renamed if required.
    #[graphql(name = "myCustomFieldName")]
    fn renamed_field() -> bool {
        true
    }

    // Deprecations also work as you'd expect.
    // Both the standard Rust syntax and a custom attribute is accepted.
    #[deprecated(note = "...")]
    fn deprecated_standard() -> bool {
        false
    }

    #[graphql(deprecated = "...")]
    fn deprecated_graphql() -> bool {
        true
    }
}

fn main() { }

Or provide a different renaming policy on a impl block for all its fields:

extern crate juniper;
use juniper::graphql_object;
struct Person;

#[graphql_object(rename_all = "none")] // disables any renaming
impl Person {
    // Now exposed as `renamed_field` in the schema
    fn renamed_field() -> bool {
        true
    }
}

fn main() {}

Customizing arguments

Method field arguments can also be customized.

They can have custom descriptions and default values.

extern crate juniper;
use juniper::graphql_object;

struct Person;

#[graphql_object]
impl Person {
    fn field1(
        &self,
        #[graphql(
            // Arguments can also be renamed if required.
            name = "arg",
            // Set a default value which will be injected if not present.
            // The default can be any valid Rust expression, including a function call, etc.
            default = true,
            // Set a description.
            description = "The first argument..."
        )]
        arg1: bool,
        // If default expression is not specified then `Default::default()` value is used.
        #[graphql(default)]
        arg2: i32,
    ) -> String {
        format!("{arg1} {arg2}")
    }
}

fn main() { }

Provide a different renaming policy on a impl block also implies for arguments:

extern crate juniper;
use juniper::graphql_object;
struct Person;

#[graphql_object(rename_all = "none")] // disables any renaming
impl Person {
    // Now exposed as `my_arg` in the schema
    fn field(my_arg: bool) -> bool {
        my_arg
    }
}

fn main() {}

More features

These, and more features, are described more thoroughly in the reference documentation.

Using contexts

The context type is a feature in Juniper that lets field resolvers access global data, most commonly database connections or authentication information. The context is usually created from a context factory. How this is defined is specific to the framework integration you're using, so check out the documentation for either the Iron or Rocket integration.

In this chapter, we'll show you how to define a context type and use it in field resolvers. Let's say that we have a simple user database in a HashMap:

#![allow(dead_code)]
use std::collections::HashMap;

struct Database {
    users: HashMap<i32, User>,
}

struct User {
    id: i32,
    name: String,
    friend_ids: Vec<i32>,
}

fn main() { }

We would like a friends field on User that returns a list of User objects. In order to write such a field though, the database must be queried.

To solve this, we mark the Database as a valid context type and assign it to the user object.

To gain access to the context, we need to specify an argument with the same type as the specified Context for the type:

extern crate juniper;
use std::collections::HashMap;
use juniper::graphql_object;

// This struct represents our context.
struct Database {
    users: HashMap<i32, User>,
}

// Mark the Database as a valid context type for Juniper
impl juniper::Context for Database {}

struct User {
    id: i32,
    name: String,
    friend_ids: Vec<i32>,
}

// Assign Database as the context type for User
#[graphql_object(context = Database)]
impl User {
    // Inject the context by specifying an argument with the context type.
    // Note: 
    //   - the type must be a reference
    //   - the name of the argument SHOULD be `context`
    fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> {
        // Use the database to lookup users
        self.friend_ids.iter()
            .map(|id| context.users.get(id).expect("Could not find user with ID"))
            .collect()
    }

    fn name(&self) -> &str { 
        self.name.as_str() 
    }

    fn id(&self) -> i32 { 
        self.id 
    }
}

fn main() { }

You only get an immutable reference to the context, so if you want to affect change to the execution, you'll need to use interior mutability using e.g. RwLock or RefCell.

Dealing with mutable references

Context cannot be specified by a mutable reference, because concurrent fields resolving may be performed. If you have something in your context that requires access by mutable reference, then you need to leverage the interior mutability for that.

For example, when using async runtime with work stealing (like tokio), which obviously requires thread safety in addition, you will need to use a corresponding async version of RwLock:

extern crate juniper;
use std::collections::HashMap;
use juniper::graphql_object;
use tokio::sync::RwLock;

struct Database {
    requested_count: HashMap<i32, i32>,
}

// Since we cannot directly implement juniper::Context
// for RwLock we use the newtype idiom
struct DatabaseContext(RwLock<Database>);

impl juniper::Context for DatabaseContext {}

struct User {
    id: i32,
    name: String
}

#[graphql_object(context=DatabaseContext)]
impl User {
    async fn times_requested<'db>(&self, context: &'db DatabaseContext) -> i32 {
        // Acquire a mutable reference and await if async RwLock is used,
        // which is necessary if context consists async operations like 
        // querying remote databases.
        // Obtain base type
        let DatabaseContext(context) = context;
        // If context is immutable use .read() on RwLock.
        let mut context = context.write().await;
        // Preform a mutable operation.
        context.requested_count.entry(self.id).and_modify(|e| { *e += 1 }).or_insert(1).clone()
    }

    fn name(&self) -> &str { 
        self.name.as_str() 
    }

    fn id(&self) -> i32 { 
        self.id 
    }
}

fn main() { }

Replace tokio::sync::RwLock with std::sync::RwLock (or similar) if you don't intend to use async resolving.

Error handling

Error handling in GraphQL can be done in multiple ways. In the following two different error handling models are discussed: field results and GraphQL schema backed errors. Each approach has its advantages. Choosing the right error handling method depends on the requirements of the application--investigating both approaches is beneficial.

Field Results

Rust provides two ways of dealing with errors: Result<T, E> for recoverable errors and panic! for unrecoverable errors. Juniper does not do anything about panicking; it will bubble up to the surrounding framework and hopefully be dealt with there.

For recoverable errors, Juniper works well with the built-in Result type, you can use the ? operator and things will generally just work as you expect them to:

extern crate juniper;
use std::{
    str,
    path::PathBuf,
    fs::{File},
    io::{Read},
};
use juniper::{graphql_object, FieldResult};

struct Example {
    filename: PathBuf,
}

#[graphql_object]
impl Example {
    fn contents(&self) -> FieldResult<String> {
        let mut file = File::open(&self.filename)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;
        Ok(contents)
    }

    fn foo() -> FieldResult<Option<String>> {
        // Some invalid bytes.
        let invalid = vec![128, 223];

        Ok(Some(str::from_utf8(&invalid)?.to_string()))
    }
}

fn main() {}

FieldResult<T> is an alias for Result<T, FieldError>, which is the error type all fields must return. By using the ? operator or try! macro, any type that implements the Display trait - which are most of the error types out there - those errors are automatically converted into FieldError.

Error payloads, null, and partial errors

Juniper's error behavior conforms to the GraphQL specification.

When a field returns an error, the field's result is replaced by null, an additional errors object is created at the top level of the response, and the execution is resumed. For example, with the previous example and the following query:

{
  example {
    contents
    foo
  }
}

If str::from_utf8 resulted in a std::str::Utf8Error, the following would be returned:

!FILENAME Response for nullable field with error

{
  "data": {
    "example": {
      contents: "<Contents of the file>",
      foo: null
    }
  },
  "errors": [
    "message": "invalid utf-8 sequence of 2 bytes from index 0",
    "locations": [{ "line": 2, "column": 4 }])
  ]
}

If an error is returned from a non-null field, such as the example above, the null value is propagated up to the first nullable parent field, or the root data object if there are no nullable fields.

For example, with the following query:

{
  example {
    contents
  }
}

If File::open() above resulted in std::io::ErrorKind::PermissionDenied, the following would be returned:

!FILENAME Response for non-null field with error and no nullable parent

{
  "errors": [
    "message": "Permission denied (os error 13)",
    "locations": [{ "line": 2, "column": 4 }])
  ]
}

Structured errors

Sometimes it is desirable to return additional structured error information to clients. This can be accomplished by implementing IntoFieldError:

#[macro_use] extern crate juniper;
use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};

enum CustomError {
    WhateverNotSet,
}

impl<S: ScalarValue> IntoFieldError<S> for CustomError {
    fn into_field_error(self) -> FieldError<S> {
        match self {
            CustomError::WhateverNotSet => FieldError::new(
                "Whatever does not exist",
                graphql_value!({
                    "type": "NO_WHATEVER"
                }),
            ),
        }
    }
}

struct Example {
    whatever: Option<bool>,
}

#[graphql_object]
impl Example {
    fn whatever(&self) -> Result<bool, CustomError> {
        if let Some(value) = self.whatever {
            return Ok(value);
        }
        Err(CustomError::WhateverNotSet)
    }
}

fn main() {}

The specified structured error information is included in the extensions key:

{
  "errors": [{
    "message": "Whatever does not exist",
    "locations": [{"line": 2, "column": 4}],
    "extensions": {
      "type": "NO_WHATEVER"
    }
  }]
}

Errors Backed by GraphQL's Schema

Rust's model of errors can be adapted for GraphQL. Rust's panic is similar to a FieldError--the whole query is aborted and nothing can be extracted (except for error related information).

Not all errors require this strict handling. Recoverable or partial errors can be put into the GraphQL schema so the client can intelligently handle them.

To implement this approach, all errors must be partitioned into two error classes:

  • Critical errors that cannot be fixed by the user (e.g. a database error).
  • Recoverable errors that can be fixed by the user (e.g. invalid input data).

Critical errors are returned from resolvers as FieldErrors (from the previous section). Non-critical errors are part of the GraphQL schema and can be handled gracefully by clients. Similar to Rust, GraphQL allows similar error models with unions (see Unions).

Example Input Validation (simple)

In this example, basic input validation is implemented with GraphQL types. Strings are used to identify the problematic field name. Errors for a particular field are also returned as a string. In this example the string contains a server-side localized error message. However, it is also possible to return a unique string identifier and have the client present a localized string to the user.

extern crate juniper;
use juniper::{graphql_object, GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationError {
    field: String,
    message: String,
}

#[derive(GraphQLObject)]
pub struct ValidationErrors {
    errors: Vec<ValidationError>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationErrors),
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
        let mut errors = Vec::new();

        if !(10 <= name.len() && name.len() <= 100) {
            errors.push(ValidationError {
                field: "name".into(),
                message: "between 10 and 100".into(),
            });
        }

        if !(1 <= quantity && quantity <= 10) {
            errors.push(ValidationError {
                field: "quantity".into(),
                message: "between 1 and 10".into(),
            });
        }

        if errors.is_empty() {
            GraphQLResult::Ok(Item { name, quantity })
        } else {
            GraphQLResult::Err(ValidationErrors { errors })
        }
    }
}

fn main() {}

Each function may have a different return type and depending on the input parameters a new result type is required. For example, adding a user requires a new result type which contains the variant Ok(User) instead of Ok(Item).

The client can send a mutation request and handle the resulting errors as shown in the following example:

{
  mutation {
    addItem(name: "", quantity: 0) {
      ... on Item {
        name
      }
      ... on ValidationErrors {
        errors {
          field
          message
        }
      }
    }
  }
}

A useful side effect of this approach is to have partially successful queries or mutations. If one resolver fails, the results of the successful resolvers are not discarded.

Example Input Validation (complex)

Instead of using strings to propagate errors, it is possible to use GraphQL's type system to describe the errors more precisely.

For each fallible input variable a field in a GraphQL object is created. The field is set if the validation for that particular field fails. You will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than before. Each resolver function has a custom ValidationResult which contains only fields provided by the function.

extern crate juniper;
use juniper::{graphql_object, GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationError {
    name: Option<String>,
    quantity: Option<String>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationError),
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
        let mut error = ValidationError {
            name: None,
            quantity: None,
        };

        if !(10 <= name.len() && name.len() <= 100) {
            error.name = Some("between 10 and 100".into());
        }

        if !(1 <= quantity && quantity <= 10) {
            error.quantity = Some("between 1 and 10".into());
        }

        if error.name.is_none() && error.quantity.is_none() {
            GraphQLResult::Ok(Item { name, quantity })
        } else {
            GraphQLResult::Err(error)
        }
    }
}

fn main() {}
{
  mutation {
    addItem {
      ... on Item {
        name
      }
      ... on ValidationErrorsItem {
        name
        quantity
      }
    }
  }
}

Expected errors are handled directly inside the query. Additionally, all non-critical errors are known in advance by both the server and the client.

Example Input Validation (complex with critical error)

Our examples so far have only included non-critical errors. Providing errors inside the GraphQL schema still allows you to return unexpected critical errors when they occur.

In the following example, a theoretical database could fail and would generate errors. Since it is not common for the database to fail, the corresponding error is returned as a critical error:

extern crate juniper;

use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};

#[derive(GraphQLObject)]
pub struct Item {
    name: String,
    quantity: i32,
}

#[derive(GraphQLObject)]
pub struct ValidationErrorItem {
    name: Option<String>,
    quantity: Option<String>,
}

#[derive(GraphQLUnion)]
pub enum GraphQLResult {
    Ok(Item),
    Err(ValidationErrorItem),
}

pub enum ApiError {
    Database,
}

impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
    fn into_field_error(self) -> FieldError<S> {
        match self {
            ApiError::Database => FieldError::new(
                "Internal database error",
                graphql_value!({
                    "type": "DATABASE"
                }),
            ),
        }
    }
}

pub struct Mutation;

#[graphql_object]
impl Mutation {
    fn addItem(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
        let mut error = ValidationErrorItem {
            name: None,
            quantity: None,
        };

        if !(10 <= name.len() && name.len() <= 100) {
            error.name = Some("between 10 and 100".into());
        }

        if !(1 <= quantity && quantity <= 10) {
            error.quantity = Some("between 1 and 10".into());
        }

        if error.name.is_none() && error.quantity.is_none() {
            Ok(GraphQLResult::Ok(Item { name, quantity }))
        } else {
            Ok(GraphQLResult::Err(error))
        }
    }
}

fn main() {}

Additional Material

The Shopify API implements a similar approach. Their API is a good reference to explore this approach in a real world application.

Comparison

The first approach discussed above--where every error is a critical error defined by FieldResult --is easier to implement. However, the client does not know what errors may occur and must instead infer what happened from the error string. This is brittle and could change over time due to either the client or server changing. Therefore, extensive integration testing between the client and server is required to maintain the implicit contract between the two.

Encoding non-critical errors in the GraphQL schema makes the contract between the client and the server explicit. This allows the client to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into the GraphQL schema requires additional code and up-front definition of non-critical errors.

Other Types

The GraphQL type system provides several types in additon to objects.

Find out more about each type below:

Enums

Enums in GraphQL are string constants grouped together to represent a set of possible values. Simple Rust enums can be converted to GraphQL enums by using a custom derive attribute:

extern crate juniper;
#[derive(juniper::GraphQLEnum)]
enum Episode {
    NewHope,
    Empire,
    Jedi,
}

fn main() {}

Juniper converts all enum variants to uppercase, so the corresponding string values for these variants are NEWHOPE, EMPIRE, and JEDI, respectively. If you want to override this, you can use the graphql attribute, similar to how it works when defining objects:

extern crate juniper;
#[derive(juniper::GraphQLEnum)]
enum Episode {
    #[graphql(name="NEW_HOPE")]
    NewHope,
    Empire,
    Jedi,
}

fn main() {}

Documentation and deprecation

Just like when defining objects, the type itself can be renamed and documented, while individual enum variants can be renamed, documented, and deprecated:

extern crate juniper;
#[derive(juniper::GraphQLEnum)]
#[graphql(name="Episode", description="An episode of Star Wars")]
enum StarWarsEpisode {
    #[graphql(deprecated="We don't really talk about this one")]
    ThePhantomMenace,

    #[graphql(name="NEW_HOPE")]
    NewHope,

    #[graphql(description="Arguably the best one in the trilogy")]
    Empire,
    Jedi,
}

fn main() {}

Supported Macro Attributes (Derive)

Name of AttributeContainer SupportField Support
context?
deprecated
description
interfaces?
name
noasync?
scalar?
skip?
✔: supported✘: not supported?: not available

Interfaces

GraphQL interfaces map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of GraphQL interfaces are Rust traits, and the main difference is that in GraphQL an interface type serves both as an abstraction and a boxed value (downcastable to concrete implementers), while in Rust, a trait is an abstraction only and to represent such a boxed value a separate type is required, like enum or trait object, because Rust trait doesn't represent a type itself, and so can have no values. This difference imposes some unintuitive and non-obvious corner cases when we try to express GraphQL interfaces in Rust, but on the other hand gives you full control over which type is backing your interface, and how it's resolved.

For implementing GraphQL interfaces Juniper provides the #[graphql_interface] macro.

Traits

Defining a trait is mandatory for defining a GraphQL interface, because this is the obvious way we describe an abstraction in Rust. All interface fields are defined as computed ones via trait methods.

extern crate juniper;
use juniper::graphql_interface;

#[graphql_interface]
trait Character {
    fn id(&self) -> &str;
}

fn main() {}

However, to return values of such interface, we should provide its implementers and the Rust type representing a boxed value of this trait. The last one can be represented in two flavors: enum and trait object.

Enum values (default)

By default, Juniper generates an enum representing the values of the defined GraphQL interface, and names it straightforwardly, {Interface}Value.

extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};

#[graphql_interface(for = [Human, Droid])] // enumerating all implementers is mandatory 
trait Character {
    fn id(&self) -> &str;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)] // notice enum name, NOT trait name
struct Human {
    id: String,
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
struct Droid {
    id: String,
}

fn main() {}

Also, enum name can be specified explicitly, if desired.

extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};

#[graphql_interface(enum = CharaterInterface, for = Human)] 
trait Character {
    fn id(&self) -> &str;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharaterInterface)]
struct Human {
    id: String,
    home_planet: String,
}

fn main() {}

Interfaces implementing other interfaces

GraphQL allows implementing interfaces on other interfaces in addition to objects.

extern crate juniper;
use juniper::{graphql_interface, graphql_object, ID};

#[graphql_interface(for = [HumanValue, Luke])]
struct Node {
    id: ID,
}

#[graphql_interface(impl = NodeValue, for = Luke)]
struct Human {
    id: ID,
    home_planet: String,
}

struct Luke {
    id: ID,
}

#[graphql_object(impl = [HumanValue, NodeValue])]
impl Luke {
    fn id(&self) -> &ID {
        &self.id
    }

    // As `String` and `&str` aren't distinguished by 
    // GraphQL spec, you can use them interchangeably.
    // Same is applied for `Cow<'a, str>`.
    //                  ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
    fn home_planet() -> &'static str {
        "Tatooine"
    }
}

fn main() {}

NOTE: Every interface has to specify all other interfaces/objects it implements or implemented for. Missing one of for = or impl = attributes is a compile-time error.

# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};

#[derive(GraphQLObject)]
pub struct ObjA {
  id: String,
}

#[graphql_interface(for = ObjA)]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 
// 'Failed to implement interface `Character` on `ObjA`: missing interface reference in implementer's `impl` attribute.'
struct Character {
  id: String,
}

fn main() {}

GraphQL subtyping and additional nullable fields

GraphQL allows implementers (both objects and other interfaces) to return "subtypes" instead of an original value. Basically, this allows you to impose additional bounds on the implementation.

Valid "subtypes" are:

  • interface implementer instead of an interface itself:
    • I implements T in place of a T;
    • Vec<I implements T> in place of a Vec<T>.
  • non-null value in place of a nullable:
    • T in place of a Option<T>;
    • Vec<T> in place of a Vec<Option<T>>.

These rules are recursively applied, so Vec<Vec<I implements T>> is a valid "subtype" of a Option<Vec<Option<Vec<Option<T>>>>>.

Also, GraphQL allows implementers to add nullable fields, which aren't present on an original interface.

extern crate juniper;
use juniper::{graphql_interface, graphql_object, ID};

#[graphql_interface(for = [HumanValue, Luke])]
struct Node {
    id: ID,
}

#[graphql_interface(for = HumanConnectionValue)]
struct Connection {
    nodes: Vec<NodeValue>,
}

#[graphql_interface(impl = NodeValue, for = Luke)]
struct Human {
    id: ID,
    home_planet: String,
}

#[graphql_interface(impl = ConnectionValue)]
struct HumanConnection {
    nodes: Vec<HumanValue>,
    //         ^^^^^^^^^^ notice not `NodeValue`
    // This can happen, because every `Human` is a `Node` too, so we are just
    // imposing additional bounds, which still can be resolved with
    // `... on Connection { nodes }`.
}

struct Luke {
    id: ID,
}

#[graphql_object(impl = [HumanValue, NodeValue])]
impl Luke {
    fn id(&self) -> &ID {
        &self.id
    }
    
    fn home_planet(language: Option<String>) -> &'static str {
        //                   ^^^^^^^^^^^^^^
        // Notice additional `null`able field, which is missing on `Human`.
        // Resolving `...on Human { homePlanet }` will provide `None` for this
        // argument.
        match language.as_deref() {
            None | Some("en") => "Tatooine",
            Some("ko") => "타투인",
            _ => todo!(),
        }
    }
}

fn main() {}

Violating GraphQL "subtyping" or additional nullable field rules is a compile-time error.

# extern crate juniper;
use juniper::{graphql_interface, graphql_object};

pub struct ObjA {
    id: String,
}

#[graphql_object(impl = CharacterValue)]
impl ObjA {
    fn id(&self, is_present: bool) -> &str {
//     ^^ the evaluated program panicked at 
//        'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument `isPresent` of type `Boolean!` 
//         isn't present on the interface and so has to be nullable.'        
        is_present.then_some(&self.id).unwrap_or("missing")
    }
}

#[graphql_interface(for = ObjA)]
struct Character {
    id: String,
}
#
# fn main() {}
# extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
pub struct ObjA {
    id: Vec<String>,
//  ^^ the evaluated program panicked at 
//     'Failed to implement interface `Character` on `ObjA`: Field `id`: implementor is expected to return a subtype of 
//      interface's return object: `[String!]!` is not a subtype of `String!`.'    
}

#[graphql_interface(for = ObjA)]
struct Character {
    id: String,
}
#
# fn main() {}

Ignoring trait methods

We may want to omit some trait methods to be assumed as GraphQL interface fields and ignore them.

extern crate juniper;
use juniper::{graphql_interface, GraphQLObject};

#[graphql_interface(for = Human)]  
trait Character {
    fn id(&self) -> &str;

    #[graphql(ignore)] // or `#[graphql(skip)]`, your choice
    fn ignored(&self) -> u32 { 0 }
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue)]
struct Human {
    id: String,
}

fn main() {}

Fields, arguments and interface customization

Similarly to GraphQL objects Juniper allows to fully customize interface fields and their arguments.

#![allow(deprecated)]
extern crate juniper;
use juniper::graphql_interface;

// Renames the interface in GraphQL schema.
#[graphql_interface(name = "MyCharacter")] 
// Describes the interface in GraphQL schema.
#[graphql_interface(description = "My own character.")]
// Usual Rust docs are supported too as GraphQL interface description, 
// but `description` attribute argument takes precedence over them, if specified.
/// This doc is absent in GraphQL schema.  
trait Character {
    // Renames the field in GraphQL schema.
    #[graphql(name = "myId")]
    // Deprecates the field in GraphQL schema.
    // Usual Rust `#[deprecated]` attribute is supported too as field deprecation,
    // but `deprecated` attribute argument takes precedence over it, if specified.
    #[graphql(deprecated = "Do not use it.")]
    // Describes the field in GraphQL schema.
    #[graphql(description = "ID of my own character.")]
    // Usual Rust docs are supported too as field description, 
    // but `description` attribute argument takes precedence over them, if specified.
    /// This description is absent in GraphQL schema.  
    fn id(
        &self,
        // Renames the argument in GraphQL schema.
        #[graphql(name = "myNum")]
        // Describes the argument in GraphQL schema.
        #[graphql(description = "ID number of my own character.")]
        // Specifies the default value for the argument.
        // The concrete value may be omitted, and the `Default::default` one 
        // will be used in such case.
        #[graphql(default = 5)]
        num: i32,
    ) -> &str;
}

fn main() {}

Renaming policies for all GraphQL interface fields and arguments are supported as well:

#![allow(deprecated)]
extern crate juniper;
use juniper::graphql_interface;

#[graphql_interface(rename_all = "none")] // disables any renaming
trait Character {
    // Now exposed as `my_id` and `my_num` in the schema
    fn my_id(&self, my_num: i32) -> &str;
}

fn main() {}

Custom context

If a Context is required in a trait method to resolve a GraphQL interface field, specify it as an argument.

extern crate juniper;
use std::collections::HashMap;
use juniper::{graphql_interface, GraphQLObject};

struct Database {
    humans: HashMap<String, Human>,
}
impl juniper::Context for Database {}

#[graphql_interface(for = Human)] // look, ma, context type is inferred! \(^o^)/
trait Character {                 // while still can be specified via `Context = ...` attribute argument
    // If a field argument is named `context` or `ctx`, it's automatically assumed
    // as a context argument.
    fn id(&self, context: &Database) -> Option<&str>;

    // Otherwise, you may mark it explicitly as a context argument.
    fn name(&self, #[graphql(context)] db: &Database) -> Option<&str>;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Context = Database)]
struct Human {
    id: String,
    name: String,
}

fn main() {}

Using executor and explicit generic scalar

If an Executor is required in a trait method to resolve a GraphQL interface field, specify it as an argument.

This requires to explicitly parametrize over ScalarValue, as Executor does so.

extern crate juniper;
use juniper::{graphql_interface, graphql_object, Executor, LookAheadMethods as _, ScalarValue};

#[graphql_interface(for = Human, Scalar = S)] // notice specifying `ScalarValue` as existing type parameter
trait Character<S: ScalarValue> {             
    // If a field argument is named `executor`, it's automatically assumed
    // as an executor argument.
    fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str;

    // Otherwise, you may mark it explicitly as an executor argument.
    fn name<'b>(
        &'b self,
        #[graphql(executor)] another: &Executor<'_, '_, (), S>,
    ) -> &'b str;
    
    fn home_planet(&self) -> &str;
}

struct Human {
    id: String,
    name: String,
    home_planet: String,
}
#[graphql_object(scalar = S: ScalarValue, impl = CharacterValue<S>)]
impl Human {
    async fn id<'a, S>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str 
    where
        S: ScalarValue,
    {
        executor.look_ahead().field_name()
    }

    async fn name<'b, S>(&'b self, #[graphql(executor)] _: &Executor<'_, '_, (), S>) -> &'b str {
        &self.name
    }
    
    fn home_planet<'c, S>(&'c self, #[graphql(executor)] _: &Executor<'_, '_, (), S>) -> &'c str {
        // Executor may not be present on the trait method  ^^^^^^^^^^^^^^^^^^^^^^^^
        &self.home_planet
    }
}

fn main() {}

ScalarValue considerations

By default, #[graphql_interface] macro generates code, which is generic over a ScalarValue type. This may introduce a problem when at least one of GraphQL interface implementers is restricted to a concrete ScalarValue type in its implementation. To resolve such problem, a concrete ScalarValue type should be specified.

extern crate juniper;
use juniper::{graphql_interface, DefaultScalarValue, GraphQLObject};

#[graphql_interface(for = [Human, Droid])]
#[graphql_interface(scalar = DefaultScalarValue)] // removing this line will fail compilation
trait Character {
    fn id(&self) -> &str;
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
struct Droid {
    id: String,
    primary_function: String,
}

fn main() {}

Input objects

Input objects are complex data structures that can be used as arguments to GraphQL fields. In Juniper, you can define input objects using a custom derive attribute, similar to simple objects and enums:

#![allow(unused_variables)]
extern crate juniper;
#[derive(juniper::GraphQLInputObject)]
struct Coordinate {
    latitude: f64,
    longitude: f64
}

struct Root;
#[derive(juniper::GraphQLObject)] struct User { name: String }

#[juniper::graphql_object]
impl Root {
    fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
        // Send coordinate to database
        // ...
unimplemented!()
    }
}

fn main() {}

Documentation and renaming

Just like the other derives, you can rename and add documentation to both the type and the fields:

#![allow(unused_variables)]
extern crate juniper;
#[derive(juniper::GraphQLInputObject)]
#[graphql(name="Coordinate", description="A position on the globe")]
struct WorldCoordinate {
    #[graphql(name="lat", description="The latitude")]
    latitude: f64,

    #[graphql(name="long", description="The longitude")]
    longitude: f64
}

struct Root;
#[derive(juniper::GraphQLObject)] struct User { name: String }

#[juniper::graphql_object]
impl Root {
    fn users_at_location(coordinate: WorldCoordinate, radius: f64) -> Vec<User> {
        // Send coordinate to database
        // ...
unimplemented!()
    }
}

fn main() {}

Scalars

Scalars are the primitive types at the leaves of a GraphQL query: numbers, strings, and booleans. You can create custom scalars to other primitive values, but this often requires coordination with the client library intended to consume the API you're building.

Since any value going over the wire is eventually transformed into JSON, you're also limited in the data types you can use.

There are two ways to define custom scalars.

  • For simple scalars that just wrap a primitive type, you can use the newtype pattern with a custom derive.
  • For more advanced use cases with custom validation, you can use the graphql_scalar proc macro.

Built-in scalars

Juniper has built-in support for:

  • i32 as Int
  • f64 as Float
  • String and &str as String
  • bool as Boolean
  • juniper::ID as ID. This type is defined in the spec as a type that is serialized as a string but can be parsed from both a string and an integer.

Note that there is no built-in support for i64/u64, as the GraphQL spec doesn't define any built-in scalars for i64/u64 by default. You may wish to leverage a custom GraphQL scalar in your schema to support them.

Third party types:

Juniper has built-in support for a few additional types from common third party crates. They are enabled via features that are on by default.

  • uuid::Uuid
  • chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime}
  • chrono_tz::Tz;
  • time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}
  • url::Url
  • bson::oid::ObjectId

Custom scalars

#[graphql(transparent)] attribute

Often, you might need a custom scalar that just wraps an existing type.

This can be done with the newtype pattern and a custom derive, similar to how serde supports this pattern with #[serde(transparent)].

extern crate juniper;

#[derive(juniper::GraphQLScalar)]
#[graphql(transparent)]
pub struct UserId(i32);

#[derive(juniper::GraphQLObject)]
struct User {
    id: UserId,
}

fn main() {}

#[derive(GraphQLScalar)] is mostly interchangeable with #[graphql_scalar] attribute:

extern crate juniper;
use juniper::graphql_scalar;

#[graphql_scalar(transparent)]
pub struct UserId {
  value: i32,
}

#[derive(juniper::GraphQLObject)]
struct User {
    id: UserId,
}

fn main() {}

That's it, you can now use UserId in your schema.

The macro also allows for more customization:

extern crate juniper;
/// You can use a doc comment to specify a description.
#[derive(juniper::GraphQLScalar)]
#[graphql(
    transparent,
    // Overwrite the GraphQL type name.
    name = "MyUserId",
    // Specify a custom description.
    // A description in the attribute will overwrite a doc comment.
    description = "My user id description",
)]
pub struct UserId(i32);

fn main() {}

All the methods used from newtype's field can be replaced with attributes:

#[graphql(to_output_with = <fn>)] attribute

use juniper::{GraphQLScalar, ScalarValue, Value};

#[derive(GraphQLScalar)]
#[graphql(to_output_with = to_output, transparent)]
struct Incremented(i32);

/// Increments [`Incremented`] before converting into a [`Value`].
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
    Value::from(v.0 + 1)
}

fn main() {}

#[graphql(from_input_with = <fn>)] attribute

use juniper::{GraphQLScalar, InputValue, ScalarValue};

#[derive(GraphQLScalar)]
#[graphql(from_input_with = Self::from_input, transparent)]
struct UserId(String);

impl UserId {
    /// Checks whether [`InputValue`] is `String` beginning with `id: ` and
    /// strips it.
    fn from_input<S>(input: &InputValue<S>) -> Result<Self, String> 
    where
        S: ScalarValue
    {
        input.as_string_value()
            .ok_or_else(|| format!("Expected `String`, found: {input}"))
            .and_then(|str| {
                str.strip_prefix("id: ")
                    .ok_or_else(|| {
                        format!(
                            "Expected `UserId` to begin with `id: `, \
                             found: {input}",
                        )
                    })
            })
            .map(|id| Self(id.to_owned()))
    }
}

fn main() {}

#[graphql(parse_token_with = <fn>] or #[graphql(parse_token(<types>)] attributes

use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, 
    ScalarValue, ScalarToken, Value
};

#[derive(GraphQLScalar)]
#[graphql(
    to_output_with = to_output,
    from_input_with = from_input,
    parse_token_with = parse_token,
//  ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`
//                   which tries to parse as `String` and then as `i32`
//                   if prior fails.
)]
enum StringOrInt {
    String(String),
    Int(i32),
}

fn to_output<S>(v: &StringOrInt) -> Value<S> 
where
    S: ScalarValue
{
    match v {
        StringOrInt::String(s) => Value::scalar(s.to_owned()),
        StringOrInt::Int(i) => Value::scalar(*i),
    }
}

fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String> 
where
    S: ScalarValue
{
    v.as_string_value()
        .map(|s| StringOrInt::String(s.into()))
        .or_else(|| v.as_int_value().map(|i| StringOrInt::Int(i)))
        .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
}

fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
    <String as ParseScalarValue<S>>::from_str(value)
        .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
}

fn main() {}

NOTE: As you can see, once you provide all 3 custom resolvers, there is no need to follow newtype pattern.

#[graphql(with = <path>)] attribute

Instead of providing all custom resolvers, you can provide path to the to_output, from_input, parse_token functions.

Path can be simply with = Self (default path where macro expects resolvers to be), in case there is an impl block with custom resolvers:

use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
    ScalarValue, ScalarToken, Value
};

#[derive(GraphQLScalar)]
// #[graphql(with = Self)] <- default behaviour
enum StringOrInt {
    String(String),
    Int(i32),
}

impl StringOrInt {
    fn to_output<S: ScalarValue>(&self) -> Value<S> {
        match self {
            Self::String(s) => Value::scalar(s.to_owned()),
            Self::Int(i) => Value::scalar(*i),
        }
    }
  
    fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
    where
        S: ScalarValue,
    {
        v.as_string_value()
            .map(|s| Self::String(s.into()))
            .or_else(|| v.as_int_value().map(Self::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }
  
    fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
    where
        S: ScalarValue,
    {
        <String as ParseScalarValue<S>>::from_str(value)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
    }
}

fn main() {}

Or it can be path to a module, where custom resolvers are located.

use juniper::{
    GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue, 
    ScalarValue, ScalarToken, Value
};

#[derive(GraphQLScalar)]
#[graphql(with = string_or_int)]
enum StringOrInt {
    String(String),
    Int(i32),
}

mod string_or_int {
    use super::*;

    pub(super) fn to_output<S>(v: &StringOrInt) -> Value<S>
    where
        S: ScalarValue,
    {
        match v {
            StringOrInt::String(s) => Value::scalar(s.to_owned()),
            StringOrInt::Int(i) => Value::scalar(*i),
        }
    }
  
    pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
    where
        S: ScalarValue,
    {
        v.as_string_value()
            .map(|s| StringOrInt::String(s.into()))
            .or_else(|| v.as_int_value().map(StringOrInt::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }
  
    pub(super) fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
    where
        S: ScalarValue,
    {
        <String as ParseScalarValue<S>>::from_str(value)
            .or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
    }
}

fn main() {}

Also, you can partially override #[graphql(with)] attribute with other custom scalars.

use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, ScalarToken, Value};

#[derive(GraphQLScalar)]
#[graphql(parse_token(String, i32))]
enum StringOrInt {
    String(String),
    Int(i32),
}

impl StringOrInt {
    fn to_output<S>(&self) -> Value<S>
    where
        S: ScalarValue,
    {
        match self {
            Self::String(s) => Value::scalar(s.to_owned()),
            Self::Int(i) => Value::scalar(*i),
        }
    }
  
    fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
    where
        S: ScalarValue,
    {
        v.as_string_value()
            .map(|s| Self::String(s.into()))
            .or_else(|| v.as_int_value().map(Self::Int))
            .ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
    }
}

fn main() {}

Using foreign types as scalars

For implementing custom scalars on foreign types there is #[graphql_scalar] attribute macro.

NOTE: To satisfy orphan rules you should provide local ScalarValue implementation.

extern crate juniper;
mod date {
   pub struct Date;
   impl std::str::FromStr for Date {
       type Err = String;

       fn from_str(_value: &str) -> Result<Self, Self::Err> { 
           unimplemented!()
       }
   }

   impl std::fmt::Display for Date {
       fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
           unimplemented!()
       }
   }
}

use juniper::DefaultScalarValue as CustomScalarValue;
use juniper::{graphql_scalar, InputValue, ScalarValue, Value};

#[graphql_scalar(
    with = date_scalar, 
    parse_token(String),
    scalar = CustomScalarValue,
//           ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation.
)]
type Date = date::Date;
//          ^^^^^^^^^^ Type from another crate.

mod date_scalar {
    use super::*;
  
    pub(super) fn to_output(v: &Date) -> Value<CustomScalarValue> {
        Value::scalar(v.to_string())
    }

    pub(super) fn from_input(v: &InputValue<CustomScalarValue>) -> Result<Date, String> {
      v.as_string_value()
          .ok_or_else(|| format!("Expected `String`, found: {v}"))
          .and_then(|s| s.parse().map_err(|e| format!("Failed to parse `Date`: {e}")))
    }
}

fn main() {}

Unions

From the server's point of view, GraphQL unions are somewhat similar to interfaces - the main difference is that they don't contain fields on their own.

The most obvious and straightforward way to represent a GraphQL union in Rust is enum. However, we also can do so either with trait or a regular struct. That's why, for implementing GraphQL unions Juniper provides:

  • #[derive(GraphQLUnion)] macro for enums and structs.
  • #[graphql_union] for traits.

Enums

Most of the time, we just need a trivial and straightforward Rust enum to represent a GraphQL union.

extern crate juniper;
extern crate derive_more;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(From, GraphQLUnion)]
enum Character {
    Human(Human),
    Droid(Droid),
}

fn main() {}

Ignoring enum variants

In some rare situations we may want to omit exposing an enum variant in the GraphQL schema.

As an example, let's consider the situation where we need to bind some type parameter T for doing interesting type-level stuff in our resolvers. To achieve this we need to have PhantomData<T>, but we don't want it exposed in the GraphQL schema.

WARNING:
It's the library user's responsibility to ensure that ignored enum variant is never returned from resolvers, otherwise resolving the GraphQL query will panic at runtime.

extern crate juniper;
extern crate derive_more;
use std::marker::PhantomData;
use derive_more::From;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(From, GraphQLUnion)]
enum Character<S> {
    Human(Human),
    Droid(Droid),
    #[from(ignore)]
    #[graphql(ignore)]  // or `#[graphql(skip)]`, your choice
    _State(PhantomData<S>),
}

fn main() {}

External resolver functions

If some custom logic is needed to resolve a GraphQL union variant, you may specify an external function to do so:

#![allow(dead_code)]
extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
#[graphql(Context = CustomContext)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = CustomContext)]
struct Droid {
    id: String,
    primary_function: String,
}

pub struct CustomContext {
    droid: Droid,
}
impl juniper::Context for CustomContext {}

#[derive(GraphQLUnion)]
#[graphql(Context = CustomContext)]
enum Character {
    Human(Human),
    #[graphql(with = Character::droid_from_context)]
    Droid(Droid),
}

impl Character {
    // NOTICE: The function signature must contain `&self` and `&Context`,
    //         and return `Option<&VariantType>`.
    fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> {
        Some(&ctx.droid)
    }
}

fn main() {}

With an external resolver function we can even declare a new GraphQL union variant where the Rust type is absent in the initial enum definition. The attribute syntax #[graphql(on VariantType = resolver_fn)] follows the GraphQL syntax for dispatching union variants.

#![allow(dead_code)]
extern crate juniper;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
#[graphql(Context = CustomContext)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = CustomContext)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = CustomContext)]
struct Ewok {
    id: String,
    is_funny: bool,
}

pub struct CustomContext {
    ewok: Ewok,
}
impl juniper::Context for CustomContext {}

#[derive(GraphQLUnion)]
#[graphql(Context = CustomContext)]
#[graphql(on Ewok = Character::ewok_from_context)]
enum Character {
    Human(Human),
    Droid(Droid),
    #[graphql(ignore)]  // or `#[graphql(skip)]`, your choice
    Ewok,
}

impl Character {
    fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> {
        if let Self::Ewok = self {
            Some(&ctx.ewok)
        } else {
            None
        }       
    }
}

fn main() {}

Structs

Using Rust structs as GraphQL unions is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a GraphQL union variant.

extern crate juniper;
use std::collections::HashMap;
use juniper::{GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Droid {
    id: String,
    primary_function: String,
}

struct Database {
    humans: HashMap<String, Human>,
    droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}

#[derive(GraphQLUnion)]
#[graphql(
    Context = Database,
    on Human = Character::get_human,
    on Droid = Character::get_droid,
)]
struct Character {
    id: String,
}

impl Character {
    fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{
        ctx.humans.get(&self.id)
    }

    fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{
        ctx.droids.get(&self.id)
    }
}

fn main() {}

Traits

To use a Rust trait definition as a GraphQL union you need to use the #[graphql_union] macro. Rust doesn't allow derive macros on traits, so using #[derive(GraphQLUnion)] on traits doesn't work.

NOTICE:
A trait has to be object safe, because schema resolvers will need to return a trait object to specify a GraphQL union behind it.

extern crate juniper;
use juniper::{graphql_union, GraphQLObject};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[graphql_union]
trait Character {
    // NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`.
    fn as_human(&self) -> Option<&Human> { None }
    fn as_droid(&self) -> Option<&Droid> { None }
}

impl Character for Human {
    fn as_human(&self) -> Option<&Human> { Some(&self) }
}

impl Character for Droid {
    fn as_droid(&self) -> Option<&Droid> { Some(&self) }
}

fn main() {}

Custom context

If a Context is required in a trait method to resolve a GraphQL union variant, specify it as an argument.

#![allow(unused_variables)]
extern crate juniper;
use std::collections::HashMap;
use juniper::{graphql_union, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Droid {
    id: String,
    primary_function: String,
}

struct Database {
    humans: HashMap<String, Human>,
    droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}

#[graphql_union(context = Database)]
trait Character {
    // NOTICE: The method signature may optionally contain `&Context`.
    fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None }
    fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None }
}

impl Character for Human {
    fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
        ctx.humans.get(&self.id)
    }
}

impl Character for Droid {
    fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> {
        ctx.droids.get(&self.id)
    }
}

fn main() {}

Ignoring trait methods

As with enums, we may want to omit some trait methods to be assumed as GraphQL union variants and ignore them.

extern crate juniper;
use juniper::{graphql_union, GraphQLObject};

#[derive(GraphQLObject)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[graphql_union]
trait Character {
    fn as_human(&self) -> Option<&Human> { None }
    fn as_droid(&self) -> Option<&Droid> { None }
    #[graphql(ignore)]  // or `#[graphql(skip)]`, your choice
    fn id(&self) -> &str;
}

impl Character for Human {
    fn as_human(&self) -> Option<&Human> { Some(&self) }
    fn id(&self) -> &str { self.id.as_str() }
}

impl Character for Droid {
    fn as_droid(&self) -> Option<&Droid> { Some(&self) }
    fn id(&self) -> &str { self.id.as_str() }
}

fn main() {}

External resolver functions

Similarly to enums and structs, it's not mandatory to use trait methods as GraphQL union variant resolvers. Instead, custom functions may be specified:

extern crate juniper;
use std::collections::HashMap;
use juniper::{graphql_union, GraphQLObject};

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
#[graphql(Context = Database)]
struct Droid {
    id: String,
    primary_function: String,
}

struct Database {
    humans: HashMap<String, Human>,
    droids: HashMap<String, Droid>,
}
impl juniper::Context for Database {}

#[graphql_union(context = Database)]
#[graphql_union(
    on Human = DynCharacter::get_human,
    on Droid = get_droid,
)]
trait Character {
    #[graphql(ignore)]  // or `#[graphql(skip)]`, your choice
    fn id(&self) -> &str;
}

impl Character for Human {
    fn id(&self) -> &str { self.id.as_str() }
}

impl Character for Droid {
    fn id(&self) -> &str { self.id.as_str() }
}

// The trait object is always `Send` and `Sync`.
type DynCharacter<'a> = dyn Character + Send + Sync + 'a;

impl<'a> DynCharacter<'a> {
    fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
        ctx.humans.get(self.id())
    }
}

// External resolver function doesn't have to be a method of a type.
// It's only a matter of the function signature to match the requirements.
fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> {
    ctx.droids.get(ch.id())
}

fn main() {}

ScalarValue considerations

By default, #[derive(GraphQLUnion)] and #[graphql_union] macros generate code, which is generic over a ScalarValue type. This may introduce a problem when at least one of GraphQL union variants is restricted to a concrete ScalarValue type in its implementation. To resolve such problem, a concrete ScalarValue type should be specified:

#![allow(dead_code)]
extern crate juniper;
use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion};

#[derive(GraphQLObject)]
#[graphql(Scalar = DefaultScalarValue)]
struct Human {
    id: String,
    home_planet: String,
}

#[derive(GraphQLObject)]
struct Droid {
    id: String,
    primary_function: String,
}

#[derive(GraphQLUnion)]
#[graphql(Scalar = DefaultScalarValue)]  // removing this line will fail compilation
enum Character {
    Human(Human),
    Droid(Droid),
}

fn main() {}

Schemas

Juniper follows a code-first approach to defining GraphQL schemas. If you would like to use a schema-first approach instead, consider juniper-from-schema for generating code from a schema file.

A schema consists of three types: a query object, a mutation object, and a subscription object. These three define the root query fields, mutations and subscriptions of the schema, respectively.

The usage of subscriptions is a little different from the mutation and query objects, so there is a specific section that discusses them.

Both query and mutation objects are regular GraphQL objects, defined like any other object in Juniper. The mutation and subscription objects, however, are optional since schemas can be read-only and do not require subscriptions. If mutation/subscription functionality is not needed, consider using EmptyMutation/EmptySubscription.

In Juniper, the RootNode type represents a schema. When the schema is first created, Juniper will traverse the entire object graph and register all types it can find. This means that if you define a GraphQL object somewhere but never reference it, it will not be exposed in a schema.

The query root

The query root is just a GraphQL object. You define it like any other GraphQL object in Juniper, most commonly using the graphql_object proc macro:

#![allow(unused_variables)]
extern crate juniper;
use juniper::{graphql_object, FieldResult, GraphQLObject};
#[derive(GraphQLObject)] struct User { name: String }
struct Root;

#[graphql_object]
impl Root {
    fn userWithUsername(username: String) -> FieldResult<Option<User>> {
        // Look up user in database...
      unimplemented!()
    }
}

fn main() { }

Mutations

Mutations are also just GraphQL objects. Each mutation is a single field that performs some mutating side-effect such as updating a database.

#![allow(unused_variables)]
extern crate juniper;
use juniper::{graphql_object, FieldResult, GraphQLObject};
#[derive(GraphQLObject)] struct User { name: String }
struct Mutations;

#[graphql_object]
impl Mutations {
    fn signUpUser(name: String, email: String) -> FieldResult<User> {
        // Validate inputs and save user in database...
      unimplemented!()
    }
}

fn main() { }

Converting a Rust schema to the GraphQL Schema Language

Many tools in the GraphQL ecosystem require the schema to be defined in the GraphQL Schema Language. You can generate a GraphQL Schema Language representation of your schema defined in Rust using the schema-language feature (on by default):

extern crate juniper;
use juniper::{
    graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
};

struct Query;

#[graphql_object]
impl Query {
    fn hello(&self) -> FieldResult<&str> {
        Ok("hello world")
    }
}

fn main() {
    // Define our schema in Rust.
    let schema = RootNode::new(
        Query,
        EmptyMutation::<()>::new(),
        EmptySubscription::<()>::new(),
    );

    // Convert the Rust schema into the GraphQL Schema Language.
    let result = schema.as_schema_language();

    let expected = "\
type Query {
  hello: String!
}

schema {
  query: Query
}
";
  #[cfg(not(target_os = "windows"))]
    assert_eq!(result, expected);
}

Note the schema-language feature may be turned off if you do not need this functionality to reduce dependencies and speed up compile times.

Adding A Server

To allow using Juniper with the HTTP server of your choice, it does not come with a built in HTTP server.

To actually get a server up and running, there are multiple official and third-party integration crates that will get you there.

Official Server Integrations

Juniper provides official integration crates for several popular Rust server libraries.

Integrating with Warp

Warp is a super-easy, composable, web server framework for warp speeds. The fundamental building block of warp is the Filter: they can be combined and composed to express rich requirements on requests. Warp is built on Hyper and works on Rust's stable channel.

Juniper's Warp integration is contained in the juniper_warp crate:

!FILENAME Cargo.toml

[dependencies]
juniper = "0.16.0"
juniper_warp = "0.8.0"

Included in the source is a small example which sets up a basic GraphQL and GraphiQL handler.

Integrating with Rocket

Rocket is a web framework for Rust that makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. Rocket does not work on Rust's stable channel and instead requires the nightly channel.

Juniper's Rocket integration is contained in the juniper_rocket crate:

!FILENAME Cargo.toml

[dependencies]
juniper = "0.16.0"
juniper_rocket = "0.9.0"

Included in the source is a small example which sets up a basic GraphQL and GraphiQL handler.

Integrating with Iron

Iron is a library that's been around for a while in the Rust sphere but lately hasn't seen much of development. Nevertheless, it's still a solid library with a familiar request/response/middleware architecture that works on Rust's stable channel.

Juniper's Iron integration is contained in the juniper_iron crate:

!FILENAME Cargo.toml

[dependencies]
juniper = "0.16.0"
juniper_iron = "0.8.0"

Included in the source is a small example which sets up a basic GraphQL and GraphiQL handler.

Basic integration

Let's start with a minimal schema and just get a GraphQL endpoint up and running. We use mount to attach the GraphQL handler at /graphql.

The context_factory function will be executed on every request and can be used to set up database connections, read session token information from cookies, and set up other global data that the schema might require.

In this example, we won't use any global data so we just return an empty value.

extern crate juniper;
extern crate juniper_iron;
extern crate iron;
extern crate mount;

use mount::Mount;
use iron::prelude::*;
use juniper::EmptyMutation;
use juniper_iron::GraphQLHandler;

fn context_factory(_: &mut Request) -> IronResult<()> {
    Ok(())
}

struct Root;

#[juniper::graphql_object]
impl Root {
    fn foo() -> String {
        "Bar".into()
    }
}

#[allow(unreachable_code, unused_variables)]
fn main() {
    let mut mount = Mount::new();

    let graphql_endpoint = GraphQLHandler::new(
        context_factory,
        Root,
        EmptyMutation::<()>::new(),
    );

    mount.mount("/graphql", graphql_endpoint);

    let chain = Chain::new(mount);

  return;
    Iron::new(chain).http("0.0.0.0:8080").unwrap();
}

Accessing data from the request

If you want to access e.g. the source IP address of the request from a field resolver, you need to pass this data using Juniper's context feature.

extern crate juniper;
extern crate juniper_iron;
extern crate iron;
use iron::prelude::*;
use std::net::SocketAddr;

struct Context {
    remote_addr: SocketAddr,
}

impl juniper::Context for Context {}

fn context_factory(req: &mut Request) -> IronResult<Context> {
    Ok(Context {
        remote_addr: req.remote_addr
    })
}

struct Root;

#[juniper::graphql_object(
    Context = Context,
)]
impl Root {
    field my_addr(context: &Context) -> String {
        format!("Hello, you're coming from {}", context.remote_addr)
    }
}

fn main() {
    let _graphql_endpoint = juniper_iron::GraphQLHandler::new(
        context_factory,
        Root,
        juniper::EmptyMutation::<Context>::new(),
    );
}

Integrating with Hyper

Hyper is a fast HTTP implementation that many other Rust web frameworks leverage. It offers asynchronous I/O via the tokio runtime and works on Rust's stable channel.

Hyper is not a higher-level web framework and accordingly does not include ergonomic features such as simple endpoint routing, baked-in HTTP responses, or reusable middleware. For GraphQL, those aren't large downsides as all POSTs and GETs usually go through a single endpoint with a few clearly-defined response payloads.

Juniper's Hyper integration is contained in the juniper_hyper crate:

!FILENAME Cargo.toml

[dependencies]
juniper = "0.16.0"
juniper_hyper = "0.9.0"

Included in the source is a small example which sets up a basic GraphQL and GraphiQL handler.

Other Examples

These examples are not officially maintained by Juniper developers.

Advanced Topics

The chapters below cover some more advanced scenarios.

Introspection

GraphQL defines a special built-in top-level field called __schema. Querying for this field allows one to introspect the schema at runtime to see what queries and mutations the GraphQL server supports.

Because introspection queries are just regular GraphQL queries, Juniper supports them natively. For example, to get all the names of the types supported one could execute the following query against Juniper:

{
  __schema {
    types {
      name
    }
  }
}

Schema introspection output as JSON

Many client libraries and tools in the GraphQL ecosystem require a complete representation of the server schema. Often this representation is in JSON and referred to as schema.json. A complete representation of the schema can be produced by issuing a specially crafted introspection query.

Juniper provides a convenience function to introspect the entire schema. The result can then be converted to JSON for use with tools and libraries such as graphql-client:

#![allow(unused_variables)]
extern crate juniper;
extern crate serde_json;
use juniper::{
    graphql_object, EmptyMutation, EmptySubscription, FieldResult, 
    GraphQLObject, IntrospectionFormat,
};

// Define our schema.

#[derive(GraphQLObject)]
struct Example {
  id: String,
}

struct Context;
impl juniper::Context for Context {}

struct Query;

#[graphql_object(context = Context)]
impl Query {
   fn example(id: String) -> FieldResult<Example> {
       unimplemented!()
   }
}

type Schema = juniper::RootNode<
    'static, 
    Query, 
    EmptyMutation<Context>, 
    EmptySubscription<Context>
>;

fn main() {
    // Create a context object.
    let ctx = Context;

    // Run the built-in introspection query.
    let (res, _errors) = juniper::introspect(
        &Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
        &ctx,
        IntrospectionFormat::default(),
    ).unwrap();

    // Convert introspection result to json.
    let json_result = serde_json::to_string_pretty(&res);
    assert!(json_result.is_ok());
}

Non-struct objects

Up until now, we've only looked at mapping structs to GraphQL objects. However, any Rust type can be mapped into a GraphQL object. In this chapter, we'll look at enums, but traits will work too - they don't have to be mapped into GraphQL interfaces.

Using Result-like enums can be a useful way of reporting e.g. validation errors from a mutation:

extern crate juniper;
use juniper::{graphql_object, GraphQLObject};
#[derive(juniper::GraphQLObject)] struct User { name: String }

#[derive(GraphQLObject)]
struct ValidationError {
    field: String,
    message: String,
}

#[allow(dead_code)]
enum SignUpResult {
    Ok(User),
    Error(Vec<ValidationError>),
}

#[graphql_object]
impl SignUpResult {
    fn user(&self) -> Option<&User> {
        match *self {
            SignUpResult::Ok(ref user) => Some(user),
            SignUpResult::Error(_) => None,
        }
    }

    fn error(&self) -> Option<&Vec<ValidationError>> {
        match *self {
            SignUpResult::Ok(_) => None,
            SignUpResult::Error(ref errors) => Some(errors)
        }
    }
}

fn main() {}

Here, we use an enum to decide whether a user's input data was valid or not, and it could be used as the result of e.g. a sign up mutation.

While this is an example of how you could use something other than a struct to represent a GraphQL object, it's also an example on how you could implement error handling for "expected" errors - errors like validation errors. There are no hard rules on how to represent errors in GraphQL, but there are some comments from one of the authors of GraphQL on how they intended "hard" field errors to be used, and how to model expected errors.

Implicit and explicit null

There are two ways that a client can submit a null argument or field in a query.

They can use a null literal:

{
    field(arg: null)
}

Or they can simply omit the argument:

{
    field
}

The former is an explicit null and the latter is an implicit null.

There are some situations where it's useful to know which one the user provided.

For example, let's say your business logic has a function that allows users to perform a "patch" operation on themselves. Let's say your users can optionally have favorite and least favorite numbers, and the input for that might look like this:

/// Updates user attributes. Fields that are `None` are left as-is.
pub struct UserPatch {
    /// If `Some`, updates the user's favorite number.
    pub favorite_number: Option<Option<i32>>,

    /// If `Some`, updates the user's least favorite number.
    pub least_favorite_number: Option<Option<i32>>,
}

fn main() {}

To set a user's favorite number to 7, you would set favorite_number to Some(Some(7)). In GraphQL, that might look like this:

mutation { patchUser(patch: { favoriteNumber: 7 }) }

To unset the user's favorite number, you would set favorite_number to Some(None). In GraphQL, that might look like this:

mutation { patchUser(patch: { favoriteNumber: null }) }

If you want to leave the user's favorite number alone, you would set it to None. In GraphQL, that might look like this:

mutation { patchUser(patch: {}) }

The last two cases rely on being able to distinguish between explicit and implicit null.

In Juniper, this can be done using the Nullable type:

extern crate juniper;
use juniper::{FieldResult, Nullable};

#[derive(juniper::GraphQLInputObject)]
struct UserPatchInput {
    pub favorite_number: Nullable<i32>,
    pub least_favorite_number: Nullable<i32>,
}

impl Into<UserPatch> for UserPatchInput {
    fn into(self) -> UserPatch {
        UserPatch {
            // The `explicit` function transforms the `Nullable` into an
            // `Option<Option<T>>` as expected by the business logic layer.
            favorite_number: self.favorite_number.explicit(),
            least_favorite_number: self.least_favorite_number.explicit(),
        }
    }
}

pub struct UserPatch {
    pub favorite_number: Option<Option<i32>>,
    pub least_favorite_number: Option<Option<i32>>,
}

struct Session;
impl Session {
    fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
}

struct Context {
    session: Session,
}
impl juniper::Context for Context {}

struct Mutation;

#[juniper::graphql_object(context = Context)]
impl Mutation {
    fn patch_user(ctx: &Context, patch: UserPatchInput) -> FieldResult<bool> {
        ctx.session.patch_user(patch.into())?;
        Ok(true)
    }
}
fn main() {}

This type functions much like Option, but has two empty variants so you can distinguish between implicit and explicit null.

Objects and generics

Yet another point where GraphQL and Rust differs is in how generics work. In Rust, almost any type could be generic - that is, take type parameters. In GraphQL, there are only two generic types: lists and non-nullables.

This poses a restriction on what you can expose in GraphQL from Rust: no generic structs can be exposed - all type parameters must be bound. For example, you can not make e.g. Result<T, E> into a GraphQL type, but you can make e.g. Result<User, String> into a GraphQL type.

Let's make a slightly more compact but generic implementation of the last chapter:

extern crate juniper;
#[derive(juniper::GraphQLObject)] struct User { name: String }
#[derive(juniper::GraphQLObject)] struct ForumPost { title: String }

#[derive(juniper::GraphQLObject)]
struct ValidationError {
    field: String,
    message: String,
}

#[allow(dead_code)]
struct MutationResult<T>(Result<T, Vec<ValidationError>>);

#[juniper::graphql_object(
    name = "UserResult",
)]
impl MutationResult<User> {
    fn user(&self) -> Option<&User> {
        self.0.as_ref().ok()
    }

    fn error(&self) -> Option<&Vec<ValidationError>> {
        self.0.as_ref().err()
    }
}

#[juniper::graphql_object(
    name = "ForumPostResult",
)]
impl MutationResult<ForumPost> {
    fn forum_post(&self) -> Option<&ForumPost> {
        self.0.as_ref().ok()
    }

    fn error(&self) -> Option<&Vec<ValidationError>> {
        self.0.as_ref().err()
    }
}

fn main() {}

Here, we've made a wrapper around Result and exposed some concrete instantiations of Result<T, E> as distinct GraphQL objects. The reason we needed the wrapper is of Rust's rules for when you can derive a trait - in this case, both Result and Juniper's internal GraphQL trait are from third-party sources.

Because we're using generics, we also need to specify a name for our instantiated types. Even if Juniper could figure out the name, MutationResult<User> wouldn't be a valid GraphQL type name.

Multiple operations per request

The GraphQL standard generally assumes there will be one server request for each client operation you want to perform (such as a query or mutation). This is conceptually simple but has the potential to be inefficent.

Some client libraries such as apollo-link-batch-http have added the ability to batch operations in a single HTTP request to save network round-trips and potentially increase performance. There are some tradeoffs that should be considered before batching requests.

Juniper's server integration crates support multiple operations in a single HTTP request using JSON arrays. This makes them compatible with client libraries that support batch operations without any special configuration.

Server integration crates maintained by others are not required to support batch requests. Batch requests aren't part of the official GraphQL specification.

Assuming an integration supports batch requests, for the following GraphQL query:

{
  hero {
    name
  }
}

The json data to POST to the server for an individual request would be:

{
  "query": "{hero{name}}"
}

And the response would be of the form:

{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

If you wanted to run the same query twice in a single HTTP request, the batched json data to POST to the server would be:

[
  {
    "query": "{hero{name}}"
  },
  {
    "query": "{hero{name}}"
  }
]

And the response would be of the form:

[
  {
    "data": {
      "hero": {
        "name": "R2-D2"
      }
    }
  },
  {
    "data": {
      "hero": {
        "name": "R2-D2"
      }
    }
  }
]

Avoiding the N+1 Problem With Dataloaders

A common issue with graphql servers is how the resolvers query their datasource. This issue results in a large number of unneccessary database queries or http requests. Say you were wanting to list a bunch of cults people were in

query {
  persons {
    id
    name
    cult {
      id
      name
    }
  }
}

What would be executed by a SQL database would be:

SELECT id, name, cult_id FROM persons;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 1;
SELECT id, name FROM cults WHERE id = 2;
SELECT id, name FROM cults WHERE id = 2;
SELECT id, name FROM cults WHERE id = 2;
# ...

Once the list of users has been returned, a separate query is run to find the cult of each user. You can see how this could quickly become a problem.

A common solution to this is to introduce a dataloader. This can be done with Juniper using the crate cksac/dataloader-rs, which has two types of dataloaders; cached and non-cached.

Cached Loader

DataLoader provides a memoization cache, after .load() is called once with a given key, the resulting value is cached to eliminate redundant loads.

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. (read more)

What does it look like?

!FILENAME Cargo.toml

[dependencies]
actix-identity = "0.4.0-beta.4"
actix-rt = "1.0"
actix-web = "2.0"
async-trait = "0.1.30"
dataloader = "0.12.0"
futures = "0.3"
juniper = "0.16.0"
postgres = "0.15.2"
// use dataloader::cached::Loader;
use dataloader::non_cached::Loader;
use dataloader::BatchFn;
use std::collections::HashMap;
use postgres::{Connection, TlsMode};
use std::env;

pub fn get_db_conn() -> Connection {
    let pg_connection_string = env::var("DATABASE_URI").expect("need a db uri");
    println!("Connecting to {pg_connection_string}");
    let conn = Connection::connect(&pg_connection_string[..], TlsMode::None).unwrap();
    println!("Connection is fine");
    conn
}

#[derive(Debug, Clone)]
pub struct Cult {
  pub id: i32,
  pub name: String,
}

pub fn get_cult_by_ids(hashmap: &mut HashMap<i32, Cult>, ids: Vec<i32>) {
  let conn = get_db_conn();
  for row in &conn
    .query("SELECT id, name FROM cults WHERE id = ANY($1)", &[&ids])
    .unwrap()
  {
    let cult = Cult {
      id: row.get(0),
      name: row.get(1),
    };
    hashmap.insert(cult.id, cult);
  }
}

pub struct CultBatcher;

#[async_trait]
impl BatchFn<i32, Cult> for CultBatcher {

    // A hashmap is used, as we need to return an array which maps each original key to a Cult.
    async fn load(&self, keys: &[i32]) -> HashMap<i32, Cult> {
        println!("load cult batch {keys:?}");
        let mut cult_hashmap = HashMap::new();
        get_cult_by_ids(&mut cult_hashmap, keys.to_vec());
        cult_hashmap
    }
}

pub type CultLoader = Loader<i32, Cult, CultBatcher>;

// To create a new loader
pub fn get_loader() -> CultLoader {
    Loader::new(CultBatcher)
      // Usually a DataLoader will coalesce all individual loads which occur 
      // within a single frame of execution before calling your batch function with all requested keys.
      // However sometimes this behavior is not desirable or optimal. 
      // Perhaps you expect requests to be spread out over a few subsequent ticks
      // See: https://github.com/cksac/dataloader-rs/issues/12 
      // More info: https://github.com/graphql/dataloader#batch-scheduling 
      // A larger yield count will allow more requests to append to batch but will wait longer before actual load.
      .with_yield_count(100)
}

#[juniper::graphql_object(Context = Context)]
impl Cult {
  //  your resolvers

  // To call the dataloader 
  pub async fn cult_by_id(ctx: &Context, id: i32) -> Cult {
    ctx.cult_loader.load(id).await
  }
}

How do I call them?

Once created, a dataloader has the async functions .load() and .load_many(). In the above example cult_loader.load(id: i32).await returns Cult. If we had used cult_loader.load_many(Vec<i32>).await it would have returned Vec<Cult>.

Where do I create my dataloaders?

Dataloaders should be created per-request to avoid risk of bugs where one user is able to load cached/batched data from another user/ outside of its authenticated scope. Creating dataloaders within individual resolvers will prevent batching from occurring and will nullify the benefits of the dataloader.

For example:

When you declare your context

use juniper;

#[derive(Clone)]
pub struct Context {
    pub cult_loader: CultLoader,
}

impl juniper::Context for Context {}

impl Context {
    pub fn new(cult_loader: CultLoader) -> Self {
        Self {
            cult_loader
        }
    }
}

Your handler for GraphQL (Note: instantiating context here keeps it per-request)

pub async fn graphql(
    st: web::Data<Arc<Schema>>,
    data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, Error> {

    // Context setup
    let cult_loader = get_loader();
    let ctx = Context::new(cult_loader);

    // Execute
    let res = data.execute(&st, &ctx).await; 
    let json = serde_json::to_string(&res).map_err(error::ErrorInternalServerError)?;

    Ok(HttpResponse::Ok()
        .content_type("application/json")
        .body(json))
}

Further Example:

For a full example using Dataloaders and Context check out jayy-lmao/rust-graphql-docker.

Subscriptions

How to achieve realtime data with GraphQL subscriptions

GraphQL subscriptions are a way to push data from the server to clients requesting real-time messages from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client, but instead of immediately returning a single answer a result is sent every time a particular event happens on the server.

In order to execute subscriptions you need a coordinator (that spawns connections) and a GraphQL object that can be resolved into a stream--elements of which will then be returned to the end user. The juniper_subscriptions crate provides a default connection implementation. Currently subscriptions are only supported on the master branch. Add the following to your Cargo.toml:

[dependencies]
juniper = "0.16.0"
juniper_subscriptions = "0.17.0"

Schema Definition

The Subscription is just a GraphQL object, similar to the query root and mutations object that you defined for the operations in your Schema. For subscriptions all fields/operations should be async and should return a Stream.

This example shows a subscription operation that returns two events, the strings Hello and World! sequentially:

use juniper::{graphql_object, graphql_subscription, FieldError};
use futures::Stream;
use std::pin::Pin;

#[derive(Clone)]
pub struct Database;
impl juniper::Context for Database {}

pub struct Query;
#[graphql_object(context = Database)]
impl Query {
   fn hello_world() -> &'static str {
       "Hello World!"
   }
}
pub struct Subscription;

type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;

#[graphql_subscription(context = Database)]
impl Subscription {
    async fn hello_world() -> StringStream {
        let stream = futures::stream::iter(vec![
            Ok(String::from("Hello")),
            Ok(String::from("World!"))
        ]);
        Box::pin(stream)
    }
}

fn main () {}

Coordinator

Subscriptions require a bit more resources than regular queries and provide a great vector for DOS attacks. This can can bring down a server easily if not handled correctly. The SubscriptionCoordinator trait provides coordination logic to enable functionality like DOS attack mitigation and resource limits.

The SubscriptionCoordinator contains the schema and can keep track of opened connections, handle subscription start and end, and maintain a global subscription id for each subscription. Each time a connection is established,
the SubscriptionCoordinator spawns a SubscriptionConnection. The SubscriptionConnection handles a single connection, providing resolver logic for a client stream as well as reconnection and shutdown logic.

While you can implement SubscriptionCoordinator yourself, Juniper contains a simple and generic implementation called Coordinator. The subscribe operation returns a Future with an Item value of a Result<Connection, GraphQLError>, where Connection is a Stream of values returned by the operation and GraphQLError is the error when the subscription fails.

#![allow(dead_code)]
extern crate futures;
extern crate juniper;
extern crate juniper_subscriptions;
extern crate serde_json;
extern crate tokio;
use juniper::{
    http::GraphQLRequest,
    graphql_object, graphql_subscription, 
    DefaultScalarValue, EmptyMutation, FieldError, 
    RootNode, SubscriptionCoordinator,
};
use juniper_subscriptions::Coordinator;
use futures::{Stream, StreamExt};
use std::pin::Pin;

#[derive(Clone)]
pub struct Database;

impl juniper::Context for Database {}

impl Database {
    fn new() -> Self {
        Self
    }
}

pub struct Query;

#[graphql_object(context = Database)]
impl Query {
    fn hello_world() -> &'static str {
        "Hello World!"
    }
}

pub struct Subscription;

type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;

#[graphql_subscription(context = Database)]
impl Subscription {
    async fn hello_world() -> StringStream {
        let stream =
            futures::stream::iter(vec![Ok(String::from("Hello")), Ok(String::from("World!"))]);
        Box::pin(stream)
    }
}
type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;

fn schema() -> Schema {
    Schema::new(Query, EmptyMutation::new(), Subscription)
}

async fn run_subscription() {
    let schema = schema();
    let coordinator = Coordinator::new(schema);
    let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
        r#"{
            "query": "subscription { helloWorld }"
        }"#,
    )
        .unwrap();
    let ctx = Database::new();
    let mut conn = coordinator.subscribe(&req, &ctx).await.unwrap();
    while let Some(result) = conn.next().await {
        println!("{}", serde_json::to_string(&result).unwrap());
    }
}

fn main() { }

Web Integration and Examples

Currently there is an example of subscriptions with warp, but it still in an alpha state. GraphQL over WS is not fully supported yet and is non-standard.