Repository: graphql-rust/juniper (language: rust)

Issue:
Macro `#[juniper::graphql_union]` does not impl `GraphQLTypeAsync`
**Describe the bug**

In the [master branch](https://github.com/graphql-rust/juniper/blob/a0b854f31c586fcf786f7ff1d5d563c5ce2d8b79/juniper_codegen/src/impl_union.rs#L155), bacause macro `juniper::graphql_union` does not impl `GraphQLTypeAsync`, the async function is not usable
Need to manually depend on `futures`
**Describe the bug**
I was starting to play with subscriptions, for which I needed to depend on the master branch

> Incidentally, a new release which includes subscriptions would be swell :)

My `Cargo.toml` therefore looks something like this:

```
[dependencies]
rocket = "0.4.4"

[dependencies.juniper]
# 2020-05-15 newer than 0.14.2
git = "https://github.com/graphql-rust/juniper.git"
rev = "91a335398313356fcd6627c00f4b9fc504714f48"

[dependencies.juniper_rocket]
# 2020-05-15 newer than 0.5.2
git = "https://github.com/graphql-rust/juniper.git"
rev = "91a335398313356fcd6627c00f4b9fc504714f48"
```

Suppose I then try to compile the following `main.rs`

```
#![feature(decl_macro, proc_macro_hygiene)]

#[derive(juniper::GraphQLEnum, Clone, Copy)]
enum Episode {
  NewHope,
  Empire,
  Jedi,
}

fn main() {}
```

I get

```
error[E0432]: unresolved import `futures`
 --> src/main.rs:3:10
  |
3 | #[derive(juniper::GraphQLEnum, Clone, Copy)]
  |          ^^^^^^^^^^^^^^^^^^^^ use of undeclared type or module `futures`
  |
  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
```

I can work around this by adding `futures = "0.3.5"` to my `Cargo.toml` and everything compiles fine. Clearly somewhere a dependency on `futures` isn't being properly expressed.

c.f. also #534 

**Additional context**

Rust toolchain `nightly-2020-05-07`



Discussion / hints:
Yep, unions and interfaces are not supported yet on async master.


Fix patch:
diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md
index a2793a626..16775eb56 100644
--- a/docs/book/content/types/unions.md
+++ b/docs/book/content/types/unions.md
@@ -1,75 +1,205 @@
-# Unions
+Unions
+======
 
-From a server's point of view, GraphQL unions are similar to interfaces: the
-only exception is that they don't contain fields on their own.
+From the server's point of view, [GraphQL unions][1] are similar to interfaces - the only exception is that they don't contain fields on their own.
+
+For implementing [GraphQL unions][1] Juniper provides:
+- `#[derive(GraphQLUnion)]` macro for enums and structs.
+- `#[graphql_union]` for traits.
 
-In Juniper, the `graphql_union!` has identical syntax to the
-[interface macro](interfaces.md), but does not support defining
-fields. Therefore, the same considerations about using traits,
-placeholder types, or enums still apply to unions. For simple
-situations, Juniper provides `#[derive(GraphQLUnion)]` for enums.
 
-If we look at the same examples as in the interfaces chapter, we see the
-similarities and the tradeoffs:
 
-## Traits
 
-### Downcasting via accessor methods
+## Enums
+
+Most of the time, we just need a trivial and straightforward Rust enum to represent a [GraphQL union][1].
 
 ```rust
-#[derive(juniper::GraphQLObject)]
+# #![allow(dead_code)]
+use derive_more::From;
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLObject)]
 struct Human {
     id: String,
     home_planet: String,
 }
 
-#[derive(juniper::GraphQLObject)]
+#[derive(GraphQLObject)]
 struct Droid {
     id: String,
     primary_function: String,
 }
 
-trait Character {
-    // Downcast methods, each concrete class will need to implement one of these
-    fn as_human(&self) -> Option<&Human> { None }
-    fn as_droid(&self) -> Option<&Droid> { None }
+#[derive(From, GraphQLUnion)]
+enum Character {
+    Human(Human),
+    Droid(Droid),
 }
+#
+# fn main() {}
+```
 
-impl Character for Human {
-    fn as_human(&self) -> Option<&Human> { Some(&self) }
+
+### 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__.
+
+```rust
+# use std::marker::PhantomData;
+use derive_more::From;
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLObject)]
+struct Human {
+    id: String,
+    home_planet: String,
 }
 
-impl Character for Droid {
-    fn as_droid(&self) -> Option<&Droid> { Some(&self) }
+#[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][1] variant, you may specify an external function to do so:
+
+```rust
+# #![allow(dead_code)]
+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),
 }
 
-#[juniper::graphql_union]
-impl<'a> GraphQLUnion for &'a dyn Character {
-    fn resolve(&self) {
-        match self {
-            Human => self.as_human(),
-            Droid => self.as_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][1] 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](https://spec.graphql.org/June2018/#example-f8163).
+
+```rust
+# #![allow(dead_code)]
+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() {}
 ```
 
-### Using an extra database lookup
 
-FIXME: This example does not compile at the moment
+
+
+## Structs
+
+Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a [GraphQL union][1] variant.
 
 ```rust
 # use std::collections::HashMap;
-#[derive(juniper::GraphQLObject)]
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLObject)]
 #[graphql(Context = Database)]
 struct Human {
     id: String,
     home_planet: String,
 }
 
-#[derive(juniper::GraphQLObject)]
+#[derive(GraphQLObject)]
 #[graphql(Context = Database)]
 struct Droid {
     id: String,
@@ -80,49 +210,92 @@ struct Database {
     humans: HashMap<String, Human>,
     droids: HashMap<String, Droid>,
 }
-
 impl juniper::Context for Database {}
 
-trait Character {
-    fn id(&self) -> &str;
+#[derive(GraphQLUnion)]
+#[graphql(
+    Context = Database,
+    on Human = Character::get_human,
+    on Droid = Character::get_droid,
+)]
+struct Character {
+    id: String,
 }
 
-impl Character for Human {
-    fn id(&self) -> &str { self.id.as_str() }
+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() {}
+```
 
-impl Character for Droid {
-    fn id(&self) -> &str { self.id.as_str() }
+
+
+
+## Traits
+
+To use a Rust trait definition as a [GraphQL union][1] you need to use the `#[graphql_union]` macro. [Rust doesn't allow derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros), so using `#[derive(GraphQLUnion)]` on traits doesn't work.
+
+> __NOTICE__:  
+> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object](https://doc.rust-lang.org/stable/reference/types/trait-object.html) to specify a [GraphQL union][1] behind it.
+
+```rust
+use juniper::{graphql_union, GraphQLObject};
+
+#[derive(GraphQLObject)]
+struct Human {
+    id: String,
+    home_planet: String,
 }
 
+#[derive(GraphQLObject)]
+struct Droid {
+    id: String,
+    primary_function: String,
+}
 
-#[juniper::graphql_union(
-    Context = Database
-)]
-impl<'a> GraphQLUnion for &'a dyn Character {
-    fn resolve(&self, context: &Database) {
-        match self {
-            Human => context.humans.get(self.id()),
-            Droid => context.droids.get(self.id()),
-        }
-    }
+#[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() {}
 ```
 
-## Placeholder objects
+
+### Custom context
+
+If a context is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument.
 
 ```rust
+# #![allow(unused_variables)]
 # use std::collections::HashMap;
-#[derive(juniper::GraphQLObject)]
+use juniper::{graphql_union, GraphQLObject};
+
+#[derive(GraphQLObject)]
 #[graphql(Context = Database)]
 struct Human {
     id: String,
     home_planet: String,
 }
 
-#[derive(juniper::GraphQLObject)]
+#[derive(GraphQLObject)]
 #[graphql(Context = Database)]
 struct Droid {
     id: String,
@@ -133,87 +306,173 @@ struct Database {
     humans: HashMap<String, Human>,
     droids: HashMap<String, Droid>,
 }
-
 impl juniper::Context for Database {}
 
-struct Character {
-    id: String,
+#[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 }
 }
 
-#[juniper::graphql_union(
-    Context = Database,
-)]
-impl GraphQLUnion for Character {
-    fn resolve(&self, context: &Database) {
-        match self {
-            Human => { context.humans.get(&self.id) },
-            Droid => { context.droids.get(&self.id) },
-        }
+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() {}
 ```
 
-## Enums (Impl)
+
+### Ignoring trait methods
+
+As with enums, we may want to omit some trait methods to be assumed as [GraphQL union][1] variants and ignore them.
 
 ```rust
-#[derive(juniper::GraphQLObject)]
+use juniper::{graphql_union, GraphQLObject};
+
+#[derive(GraphQLObject)]
 struct Human {
     id: String,
     home_planet: String,
 }
 
-#[derive(juniper::GraphQLObject)]
+#[derive(GraphQLObject)]
 struct Droid {
     id: String,
     primary_function: String,
 }
 
-# #[allow(dead_code)]
-enum Character {
-    Human(Human),
-    Droid(Droid),
+#[graphql_union]
+trait Character {
+    fn as_human(&self) -> Option<&Human> { None }
+    fn as_droid(&self) -> Option<&Droid> { None }
+    #[graphql_union(ignore)]  // or `#[graphql_union(skip)]`, your choice
+    fn id(&self) -> &str;
 }
 
-#[juniper::graphql_union]
-impl Character {
-    fn resolve(&self) {
-        match self {
-            Human => { match *self { Character::Human(ref h) => Some(h), _ => None } },
-            Droid => { match *self { Character::Droid(ref d) => Some(d), _ => None } },
-        }
+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][1] variant resolvers. Instead, custom functions may be specified:
+
+```rust
+# 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_union(ignore)]  // or `#[graphql_union(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() {}
 ```
 
-## Enums (Derive)
 
-This example is similar to `Enums (Impl)`. To successfully use the
-derive macro, ensure that each variant of the enum has a different
-type. Since each variant is different, the device macro provides
-`std::convert::Into<T>` converter for each variant.
+
+
+## `ScalarValue` considerations
+
+By default, `#[derive(GraphQLUnion)]` and `#[graphql_union]` macros generate code, which is generic over a [`ScalarValue`][2] type. This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`][2] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][2] type should be specified:
 
 ```rust
-#[derive(juniper::GraphQLObject)]
+# #![allow(dead_code)]
+use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLObject)]
+#[graphql(Scalar = DefaultScalarValue)]
 struct Human {
     id: String,
     home_planet: String,
 }
 
-#[derive(juniper::GraphQLObject)]
+#[derive(GraphQLObject)]
 struct Droid {
     id: String,
     primary_function: String,
 }
 
-#[derive(juniper::GraphQLUnion)]
+#[derive(GraphQLUnion)]
+#[graphql(Scalar = DefaultScalarValue)]  // removing this line will fail compilation
 enum Character {
     Human(Human),
     Droid(Droid),
 }
-
+#
 # fn main() {}
 ```
+
+
+
+
+
+[1]: https://spec.graphql.org/June2018/#sec-Unions
+[2]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 35e6dabfb..154c06d01 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -2,7 +2,7 @@
 
 ## Features
 
-- Support raw identifiers in field and argument names. (#[object] macro)
+- Support raw identifiers in field and argument names. (`#[object]` macro)
 
 - Most error types now implement `std::error::Error`:
   - `GraphQLError`
@@ -29,10 +29,21 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
 
 - Derive macro `GraphQLEnum` supports custom context (see [#621](https://github.com/graphql-rust/juniper/pull/621))
 
+- Reworked `#[derive(GraphQLUnion)]` macro ([#666]):
+    - Applicable to enums and structs.
+    - Supports custom resolvers.
+    - Supports generics.
+    - Supports multiple `#[graphql]` attributes.
+- New `#[graphql_union]` macro ([#666]):
+    - Applicable to traits.
+    - Supports custom resolvers.
+    - Supports generics.
+    - Supports multiple `#[graphql_union]` attributes.
+
 - Better error messages for all proc macros (see
   [#631](https://github.com/graphql-rust/juniper/pull/631)
 
--  Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631))
+- Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631))
 
 ## Breaking Changes
 
@@ -45,10 +56,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
 
 - Remove deprecated `ScalarValue` custom derive (renamed to GraphQLScalarValue)
 
-- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro
+- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro and custom resolvers for the `#[derive(GraphQLUnion)]` macro.
+- The `#[derive(GraphQLUnion)]` macro doesn't generate `From` impls for enum variants anymore. Consider using the [`derive_more`](https//docs.rs/derive_more) crate directly ([#666]).
 
-- ScalarRefValue trait removed
-  Trait was not required.
+- `ScalarRefValue` trait removed. Trait was not required.
 
 - Changed return type of GraphQLType::resolve to `ExecutionResult`
   This was done to unify the return type of all resolver methods
@@ -59,7 +70,7 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
   add subscription type to `RootNode`,
   add subscription endpoint to `playground_source()`
 
-- Putting a scalar type into a string is not allowed anymore, e..g,
+- Putting a scalar type into a string is not allowed anymore, e.g.
   `#[graphql(scalar = "DefaultScalarValue")]`. Only
   `#[derive(GraphQLInputObject)]` supported this syntax. The
   refactoring of GraphQLInputObject allowed to drop the support
@@ -75,6 +86,8 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
 
 - When using LookAheadMethods to access child selections, children are always found using their alias if it exists rather than their name (see [#662](https://github.com/graphql-rust/juniper/pull/631)). These methods are also deprecated in favour of the new `children` method.
 
+[#666]: https://github.com/graphql-rust/juniper/pull/666
+
 # [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)
 
 - Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)
diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml
index 69e970a61..a8bd7b46a 100644
--- a/juniper/Cargo.toml
+++ b/juniper/Cargo.toml
@@ -43,6 +43,7 @@ futures = "0.3.1"
 indexmap = { version = "1.0.0", features = ["serde-1"] }
 serde = { version = "1.0.8", features = ["derive"] }
 serde_json = { version="1.0.2", optional = true }
+static_assertions = "1.1"
 url = { version = "2", optional = true }
 uuid = { version = "0.8", optional = true }
 
diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs
index d1378c17c..998c7d50f 100644
--- a/juniper/src/executor/mod.rs
+++ b/juniper/src/executor/mod.rs
@@ -242,8 +242,9 @@ impl<S> IntoFieldError<S> for FieldError<S> {
 }
 
 #[doc(hidden)]
-pub trait IntoResolvable<'a, S, T: GraphQLType<S>, C>: Sized
+pub trait IntoResolvable<'a, S, T, C>
 where
+    T: GraphQLType<S>,
     S: ScalarValue,
 {
     #[doc(hidden)]
@@ -404,7 +405,7 @@ where
     pub fn resolve_with_ctx<NewCtxT, T>(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult<S>
     where
         NewCtxT: FromContext<CtxT>,
-        T: GraphQLType<S, Context = NewCtxT>,
+        T: GraphQLType<S, Context = NewCtxT> + ?Sized,
     {
         self.replaced_context(<NewCtxT as FromContext<CtxT>>::from(self.context))
             .resolve(info, value)
@@ -413,7 +414,7 @@ where
     /// Resolve a single arbitrary value into an `ExecutionResult`
     pub fn resolve<T>(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult<S>
     where
-        T: GraphQLType<S, Context = CtxT>,
+        T: GraphQLType<S, Context = CtxT> + ?Sized,
     {
         value.resolve(info, self.current_selection_set, self)
     }
@@ -421,7 +422,7 @@ where
     /// Resolve a single arbitrary value into an `ExecutionResult`
     pub async fn resolve_async<T>(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult<S>
     where
-        T: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
+        T: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + ?Sized,
         T::TypeInfo: Send + Sync,
         CtxT: Send + Sync,
         S: Send + Sync,
@@ -468,18 +469,15 @@ where
     /// If the field fails to resolve, `null` will be returned.
     pub async fn resolve_into_value_async<T>(&self, info: &T::TypeInfo, value: &T) -> Value<S>
     where
-        T: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
+        T: crate::GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + ?Sized,
         T::TypeInfo: Send + Sync,
         CtxT: Send + Sync,
         S: Send + Sync,
     {
-        match self.resolve_async(info, value).await {
-            Ok(v) => v,
-            Err(e) => {
-                self.push_error(e);
-                Value::null()
-            }
-        }
+        self.resolve_async(info, value).await.unwrap_or_else(|e| {
+            self.push_error(e);
+            Value::null()
+        })
     }
 
     /// Derive a new executor by replacing the context
@@ -1103,7 +1101,7 @@ where
     /// construct its metadata and store it.
     pub fn get_type<T>(&mut self, info: &T::TypeInfo) -> Type<'r>
     where
-        T: GraphQLType<S>,
+        T: GraphQLType<S> + ?Sized,
     {
         if let Some(name) = T::name(info) {
             let validated_name = name.parse::<Name>().unwrap();
@@ -1124,7 +1122,7 @@ where
     /// Create a field with the provided name
     pub fn field<T>(&mut self, name: &str, info: &T::TypeInfo) -> Field<'r, S>
     where
-        T: GraphQLType<S>,
+        T: GraphQLType<S> + ?Sized,
     {
         Field {
             name: name.to_owned(),
@@ -1156,7 +1154,7 @@ where
     /// Create an argument with the provided name
     pub fn arg<T>(&mut self, name: &str, info: &T::TypeInfo) -> Argument<'r, S>
     where
-        T: GraphQLType<S> + FromInputValue<S>,
+        T: GraphQLType<S> + FromInputValue<S> + ?Sized,
     {
         Argument::new(name, self.get_type::<T>(info))
     }
@@ -1172,7 +1170,7 @@ where
         info: &T::TypeInfo,
     ) -> Argument<'r, S>
     where
-        T: GraphQLType<S> + ToInputValue<S> + FromInputValue<S>,
+        T: GraphQLType<S> + ToInputValue<S> + FromInputValue<S> + ?Sized,
     {
         Argument::new(name, self.get_type::<Option<T>>(info)).default_value(value.to_input_value())
     }
@@ -1188,20 +1186,23 @@ where
     /// This expects the type to implement `FromInputValue`.
     pub fn build_scalar_type<T>(&mut self, info: &T::TypeInfo) -> ScalarMeta<'r, S>
     where
-        T: FromInputValue<S> + GraphQLType<S> + ParseScalarValue<S> + 'r,
+        T: FromInputValue<S> + GraphQLType<S> + ParseScalarValue<S> + ?Sized + 'r,
     {
         let name = T::name(info).expect("Scalar types must be named. Implement name()");
         ScalarMeta::new::<T>(Cow::Owned(name.to_string()))
     }
 
     /// Create a list meta type
-    pub fn build_list_type<T: GraphQLType<S>>(&mut self, info: &T::TypeInfo) -> ListMeta<'r> {
+    pub fn build_list_type<T: GraphQLType<S> + ?Sized>(
+        &mut self,
+        info: &T::TypeInfo,
+    ) -> ListMeta<'r> {
         let of_type = self.get_type::<T>(info);
         ListMeta::new(of_type)
     }
 
     /// Create a nullable meta type
-    pub fn build_nullable_type<T: GraphQLType<S>>(
+    pub fn build_nullable_type<T: GraphQLType<S> + ?Sized>(
         &mut self,
         info: &T::TypeInfo,
     ) -> NullableMeta<'r> {
@@ -1219,7 +1220,7 @@ where
         fields: &[Field<'r, S>],
     ) -> ObjectMeta<'r, S>
     where
-        T: GraphQLType<S>,
+        T: GraphQLType<S> + ?Sized,
     {
         let name = T::name(info).expect("Object types must be named. Implement name()");
 
@@ -1235,7 +1236,7 @@ where
         values: &[EnumValue],
     ) -> EnumMeta<'r, S>
     where
-        T: FromInputValue<S> + GraphQLType<S>,
+        T: FromInputValue<S> + GraphQLType<S> + ?Sized,
     {
         let name = T::name(info).expect("Enum types must be named. Implement name()");
 
@@ -1250,7 +1251,7 @@ where
         fields: &[Field<'r, S>],
     ) -> InterfaceMeta<'r, S>
     where
-        T: GraphQLType<S>,
+        T: GraphQLType<S> + ?Sized,
     {
         let name = T::name(info).expect("Interface types must be named. Implement name()");
 
@@ -1262,7 +1263,7 @@ where
     /// Create a union meta type
     pub fn build_union_type<T>(&mut self, info: &T::TypeInfo, types: &[Type<'r>]) -> UnionMeta<'r>
     where
-        T: GraphQLType<S>,
+        T: GraphQLType<S> + ?Sized,
     {
         let name = T::name(info).expect("Union types must be named. Implement name()");
 
@@ -1276,7 +1277,7 @@ where
         args: &[Argument<'r, S>],
     ) -> InputObjectMeta<'r, S>
     where
-        T: FromInputValue<S> + GraphQLType<S>,
+        T: FromInputValue<S> + GraphQLType<S> + ?Sized,
     {
         let name = T::name(info).expect("Input object types must be named. Implement name()");
 
diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs
index 32e26f3bc..14bf1fa52 100644
--- a/juniper/src/lib.rs
+++ b/juniper/src/lib.rs
@@ -93,6 +93,8 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
 #![doc(html_root_url = "https://docs.rs/juniper/0.14.2")]
 #![warn(missing_docs)]
 
+use std::fmt;
+
 #[doc(hidden)]
 pub extern crate serde;
 
@@ -111,6 +113,13 @@ extern crate uuid;
 #[cfg(any(test, feature = "bson"))]
 extern crate bson;
 
+// These are required by the code generated via the `juniper_codegen` macros.
+#[doc(hidden)]
+pub use {futures, static_assertions as sa};
+
+#[doc(inline)]
+pub use futures::future::BoxFuture;
+
 // Depend on juniper_codegen and re-export everything in it.
 // This allows users to just depend on juniper and get the derive
 // functionality automatically.
@@ -124,7 +133,7 @@ pub use juniper_codegen::{
 use juniper_codegen::{
     graphql_object_internal, graphql_scalar_internal, graphql_subscription_internal,
     graphql_union_internal, GraphQLEnumInternal, GraphQLInputObjectInternal,
-    GraphQLScalarValueInternal,
+    GraphQLScalarValueInternal, GraphQLUnionInternal,
 };
 
 #[macro_use]
@@ -161,7 +170,6 @@ use crate::{
     parser::{parse_document_source, ParseError, Spanning},
     validation::{validate_input_values, visit_all_rules, ValidatorContext},
 };
-use std::fmt;
 
 pub use crate::{
     ast::{FromInputValue, InputValue, Selection, ToInputValue, Type},
@@ -179,7 +187,7 @@ pub use crate::{
     types::{
         async_await::GraphQLTypeAsync,
         base::{Arguments, GraphQLType, TypeKind},
-        marker,
+        marker::{self, GraphQLUnion},
         scalars::{EmptyMutation, EmptySubscription, ID},
         subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator},
     },
@@ -187,9 +195,6 @@ pub use crate::{
     value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value},
 };
 
-/// A pinned, boxed future that can be polled.
-pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + 'a + Send>>;
-
 /// An error that prevented query execution
 #[derive(Debug, PartialEq)]
 #[allow(missing_docs)]
diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs
index d86210356..b024ed05e 100644
--- a/juniper/src/schema/meta.rs
+++ b/juniper/src/schema/meta.rs
@@ -572,8 +572,10 @@ where
     S: ScalarValue,
 {
     /// Build a new input type with the specified name and input fields
-    pub fn new<T: FromInputValue<S>>(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self
-where {
+    pub fn new<T>(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self
+    where
+        T: FromInputValue<S> + ?Sized,
+    {
         InputObjectMeta {
             name,
             description: None,
diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs
index ef5f9ec47..156c2ab52 100644
--- a/juniper/src/types/async_await.rs
+++ b/juniper/src/types/async_await.rs
@@ -98,7 +98,7 @@ fn resolve_selection_set_into_async<'a, 'e, T, CtxT, S>(
     executor: &'e Executor<'e, 'e, CtxT, S>,
 ) -> BoxFuture<'a, Value<S>>
 where
-    T: GraphQLTypeAsync<S, Context = CtxT>,
+    T: GraphQLTypeAsync<S, Context = CtxT> + ?Sized,
     T::TypeInfo: Send + Sync,
     S: ScalarValue + Send + Sync,
     CtxT: Send + Sync,
@@ -129,7 +129,7 @@ pub(crate) async fn resolve_selection_set_into_async_recursive<'a, T, CtxT, S>(
     executor: &'a Executor<'a, 'a, CtxT, S>,
 ) -> Value<S>
 where
-    T: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync,
+    T: GraphQLTypeAsync<S, Context = CtxT> + Send + Sync + ?Sized,
     T::TypeInfo: Send + Sync,
     S: ScalarValue + Send + Sync,
     CtxT: Send + Sync,
diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs
index 1ce3754f4..c80188ed0 100644
--- a/juniper/src/types/base.rs
+++ b/juniper/src/types/base.rs
@@ -230,7 +230,7 @@ impl GraphQLType<DefaultScalarValue> for User
 ```
 
 */
-pub trait GraphQLType<S = DefaultScalarValue>: Sized
+pub trait GraphQLType<S = DefaultScalarValue>
 where
     S: ScalarValue,
 {
@@ -355,7 +355,7 @@ pub(crate) fn resolve_selection_set_into<T, CtxT, S>(
     result: &mut Object<S>,
 ) -> bool
 where
-    T: GraphQLType<S, Context = CtxT>,
+    T: GraphQLType<S, Context = CtxT> + ?Sized,
     S: ScalarValue,
 {
     let meta_type = executor
diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs
index e587dee51..589624028 100644
--- a/juniper/src/types/containers.rs
+++ b/juniper/src/types/containers.rs
@@ -1,15 +1,11 @@
 use crate::{
     ast::{FromInputValue, InputValue, Selection, ToInputValue},
-    executor::ExecutionResult,
+    executor::{ExecutionResult, Executor, Registry},
     schema::meta::MetaType,
+    types::{async_await::GraphQLTypeAsync, base::GraphQLType},
     value::{ScalarValue, Value},
 };
 
-use crate::{
-    executor::{Executor, Registry},
-    types::base::GraphQLType,
-};
-
 impl<S, T, CtxT> GraphQLType<S> for Option<T>
 where
     S: ScalarValue,
@@ -42,6 +38,30 @@ where
     }
 }
 
+impl<S, T, CtxT> GraphQLTypeAsync<S> for Option<T>
+where
+    T: GraphQLTypeAsync<S, Context = CtxT>,
+    T::TypeInfo: Send + Sync,
+    S: ScalarValue + Send + Sync,
+    CtxT: Send + Sync,
+{
+    fn resolve_async<'a>(
+        &'a self,
+        info: &'a Self::TypeInfo,
+        _selection_set: Option<&'a [Selection<S>]>,
+        executor: &'a Executor<Self::Context, S>,
+    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
+        let f = async move {
+            let value = match *self {
+                Some(ref obj) => executor.resolve_into_value_async(info, obj).await,
+                None => Value::null(),
+            };
+            Ok(value)
+        };
+        Box::pin(f)
+    }
+}
+
 impl<S, T> FromInputValue<S> for Option<T>
 where
     T: FromInputValue<S>,
@@ -50,10 +70,7 @@ where
     fn from_input_value<'a>(v: &'a InputValue<S>) -> Option<Option<T>> {
         match v {
             &InputValue::Null => Some(None),
-            v => match v.convert() {
-                Some(x) => Some(Some(x)),
-                None => None,
-            },
+            v => v.convert().map(Some),
         }
     }
 }
@@ -100,6 +117,24 @@ where
     }
 }
 
+impl<S, T, CtxT> GraphQLTypeAsync<S> for Vec<T>
+where
+    T: GraphQLTypeAsync<S, Context = CtxT>,
+    T::TypeInfo: Send + Sync,
+    S: ScalarValue + Send + Sync,
+    CtxT: Send + Sync,
+{
+    fn resolve_async<'a>(
+        &'a self,
+        info: &'a Self::TypeInfo,
+        _selection_set: Option<&'a [Selection<S>]>,
+        executor: &'a Executor<Self::Context, S>,
+    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
+        let f = resolve_into_list_async(executor, info, self.iter());
+        Box::pin(f)
+    }
+}
+
 impl<T, S> FromInputValue<S> for Vec<T>
 where
     T: FromInputValue<S>,
@@ -117,13 +152,7 @@ where {
                     None
                 }
             }
-            ref other => {
-                if let Some(e) = other.convert() {
-                    Some(vec![e])
-                } else {
-                    None
-                }
-            }
+            ref other => other.convert().map(|e| vec![e]),
         }
     }
 }
@@ -134,11 +163,11 @@ where
     S: ScalarValue,
 {
     fn to_input_value(&self) -> InputValue<S> {
-        InputValue::list(self.iter().map(|v| v.to_input_value()).collect())
+        InputValue::list(self.iter().map(T::to_input_value).collect())
     }
 }
 
-impl<'a, S, T, CtxT> GraphQLType<S> for &'a [T]
+impl<S, T, CtxT> GraphQLType<S> for [T]
 where
     S: ScalarValue,
     T: GraphQLType<S, Context = CtxT>,
@@ -167,25 +196,43 @@ where
     }
 }
 
+impl<S, T, CtxT> GraphQLTypeAsync<S> for [T]
+where
+    T: GraphQLTypeAsync<S, Context = CtxT>,
+    T::TypeInfo: Send + Sync,
+    S: ScalarValue + Send + Sync,
+    CtxT: Send + Sync,
+{
+    fn resolve_async<'a>(
+        &'a self,
+        info: &'a Self::TypeInfo,
+        _selection_set: Option<&'a [Selection<S>]>,
+        executor: &'a Executor<Self::Context, S>,
+    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
+        let f = resolve_into_list_async(executor, info, self.iter());
+        Box::pin(f)
+    }
+}
+
 impl<'a, T, S> ToInputValue<S> for &'a [T]
 where
     T: ToInputValue<S>,
     S: ScalarValue,
 {
     fn to_input_value(&self) -> InputValue<S> {
-        InputValue::list(self.iter().map(|v| v.to_input_value()).collect())
+        InputValue::list(self.iter().map(T::to_input_value).collect())
     }
 }
 
-fn resolve_into_list<S, T, I>(
+fn resolve_into_list<'t, S, T, I>(
     executor: &Executor<T::Context, S>,
     info: &T::TypeInfo,
     iter: I,
 ) -> ExecutionResult<S>
 where
     S: ScalarValue,
-    I: Iterator<Item = T> + ExactSizeIterator,
-    T: GraphQLType<S>,
+    I: Iterator<Item = &'t T> + ExactSizeIterator,
+    T: GraphQLType<S> + ?Sized + 't,
 {
     let stop_on_null = executor
         .current_type()
@@ -195,30 +242,26 @@ where
     let mut result = Vec::with_capacity(iter.len());
 
     for o in iter {
-        match executor.resolve(info, &o) {
-            Ok(value) => {
-                if stop_on_null && value.is_null() {
-                    return Ok(value);
-                } else {
-                    result.push(value)
-                }
-            }
-            Err(e) => return Err(e),
+        let val = executor.resolve(info, o)?;
+        if stop_on_null && val.is_null() {
+            return Ok(val);
+        } else {
+            result.push(val)
         }
     }
 
     Ok(Value::list(result))
 }
 
-async fn resolve_into_list_async<'a, S, T, I>(
+async fn resolve_into_list_async<'a, 't, S, T, I>(
     executor: &'a Executor<'a, 'a, T::Context, S>,
     info: &'a T::TypeInfo,
     items: I,
 ) -> ExecutionResult<S>
 where
     S: ScalarValue + Send + Sync,
-    I: Iterator<Item = T> + ExactSizeIterator,
-    T: crate::GraphQLTypeAsync<S>,
+    I: Iterator<Item = &'t T> + ExactSizeIterator,
+    T: GraphQLTypeAsync<S> + ?Sized + 't,
     T::TypeInfo: Send + Sync,
     T::Context: Send + Sync,
 {
@@ -231,8 +274,7 @@ where
         .expect("Current type is not a list type")
         .is_non_null();
 
-    let iter =
-        items.map(|item| async move { executor.resolve_into_value_async(info, &item).await });
+    let iter = items.map(|item| async move { executor.resolve_into_value_async(info, item).await });
     let mut futures = FuturesOrdered::from_iter(iter);
 
     let mut values = Vec::with_capacity(futures.len());
@@ -245,63 +287,3 @@ where
 
     Ok(Value::list(values))
 }
-
-impl<S, T, CtxT> crate::GraphQLTypeAsync<S> for Vec<T>
-where
-    T: crate::GraphQLTypeAsync<S, Context = CtxT>,
-    T::TypeInfo: Send + Sync,
-    S: ScalarValue + Send + Sync,
-    CtxT: Send + Sync,
-{
-    fn resolve_async<'a>(
-        &'a self,
-        info: &'a Self::TypeInfo,
-        _selection_set: Option<&'a [Selection<S>]>,
-        executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
-        let f = resolve_into_list_async(executor, info, self.iter());
-        Box::pin(f)
-    }
-}
-
-impl<S, T, CtxT> crate::GraphQLTypeAsync<S> for &[T]
-where
-    T: crate::GraphQLTypeAsync<S, Context = CtxT>,
-    T::TypeInfo: Send + Sync,
-    S: ScalarValue + Send + Sync,
-    CtxT: Send + Sync,
-{
-    fn resolve_async<'a>(
-        &'a self,
-        info: &'a Self::TypeInfo,
-        _selection_set: Option<&'a [Selection<S>]>,
-        executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
-        let f = resolve_into_list_async(executor, info, self.iter());
-        Box::pin(f)
-    }
-}
-
-impl<S, T, CtxT> crate::GraphQLTypeAsync<S> for Option<T>
-where
-    T: crate::GraphQLTypeAsync<S, Context = CtxT>,
-    T::TypeInfo: Send + Sync,
-    S: ScalarValue + Send + Sync,
-    CtxT: Send + Sync,
-{
-    fn resolve_async<'a>(
-        &'a self,
-        info: &'a Self::TypeInfo,
-        _selection_set: Option<&'a [Selection<S>]>,
-        executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
-        let f = async move {
-            let value = match *self {
-                Some(ref obj) => executor.resolve_into_value_async(info, obj).await,
-                None => Value::null(),
-            };
-            Ok(value)
-        };
-        Box::pin(f)
-    }
-}
diff --git a/juniper/src/types/marker.rs b/juniper/src/types/marker.rs
index e3d42432c..5e2fc909e 100644
--- a/juniper/src/types/marker.rs
+++ b/juniper/src/types/marker.rs
@@ -23,6 +23,30 @@ pub trait GraphQLObjectType<S: ScalarValue>: GraphQLType<S> {
     fn mark() {}
 }
 
+/// Maker trait for [GraphQL unions][1].
+///
+/// This trait extends the [`GraphQLType`] and is only used to mark [union][1]. During compile this
+/// addition information is required to prevent unwanted structure compiling. If an object requires
+/// this trait instead of the [`GraphQLType`], then it explicitly requires [GraphQL unions][1].
+/// Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and [interfaces][6]) are
+/// not allowed.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+/// [2]: https://spec.graphql.org/June2018/#sec-Scalars
+/// [3]: https://spec.graphql.org/June2018/#sec-Enums
+/// [4]: https://spec.graphql.org/June2018/#sec-Objects
+/// [5]: https://spec.graphql.org/June2018/#sec-Input-Objects
+/// [6]: https://spec.graphql.org/June2018/#sec-Interfaces
+pub trait GraphQLUnion: GraphQLType {
+    /// An arbitrary function without meaning.
+    ///
+    /// May contain compile timed check logic which ensures that types are used correctly according
+    /// to the [GraphQL specification][1].
+    ///
+    /// [1]: https://spec.graphql.org/June2018/
+    fn mark() {}
+}
+
 /// Marker trait for types which can be used as output types.
 ///
 /// The GraphQL specification differentiates between input and output
diff --git a/juniper/src/types/pointers.rs b/juniper/src/types/pointers.rs
index b599b2cae..68274fad3 100644
--- a/juniper/src/types/pointers.rs
+++ b/juniper/src/types/pointers.rs
@@ -1,17 +1,21 @@
-use crate::ast::{FromInputValue, InputValue, Selection, ToInputValue};
 use std::{fmt::Debug, sync::Arc};
 
 use crate::{
+    ast::{FromInputValue, InputValue, Selection, ToInputValue},
     executor::{ExecutionResult, Executor, Registry},
     schema::meta::MetaType,
-    types::base::{Arguments, GraphQLType},
+    types::{
+        async_await::GraphQLTypeAsync,
+        base::{Arguments, GraphQLType},
+    },
     value::ScalarValue,
+    BoxFuture,
 };
 
 impl<S, T, CtxT> GraphQLType<S> for Box<T>
 where
     S: ScalarValue,
-    T: GraphQLType<S, Context = CtxT>,
+    T: GraphQLType<S, Context = CtxT> + ?Sized,
 {
     type Context = CtxT;
     type TypeInfo = T::TypeInfo;
@@ -57,6 +61,23 @@ where
     }
 }
 
+impl<S, T, CtxT> crate::GraphQLTypeAsync<S> for Box<T>
+where
+    T: GraphQLTypeAsync<S, Context = CtxT> + ?Sized,
+    T::TypeInfo: Send + Sync,
+    S: ScalarValue + Send + Sync,
+    CtxT: Send + Sync,
+{
+    fn resolve_async<'a>(
+        &'a self,
+        info: &'a Self::TypeInfo,
+        selection_set: Option<&'a [Selection<S>]>,
+        executor: &'a Executor<Self::Context, S>,
+    ) -> BoxFuture<'a, ExecutionResult<S>> {
+        (**self).resolve_async(info, selection_set, executor)
+    }
+}
+
 impl<T, S> FromInputValue<S> for Box<T>
 where
     S: ScalarValue,
@@ -83,7 +104,7 @@ where
 impl<'e, S, T, CtxT> GraphQLType<S> for &'e T
 where
     S: ScalarValue,
-    T: GraphQLType<S, Context = CtxT>,
+    T: GraphQLType<S, Context = CtxT> + ?Sized,
 {
     type Context = CtxT;
     type TypeInfo = T::TypeInfo;
@@ -129,10 +150,10 @@ where
     }
 }
 
-impl<'e, S, T> crate::GraphQLTypeAsync<S> for &'e T
+impl<'e, S, T> GraphQLTypeAsync<S> for &'e T
 where
     S: ScalarValue + Send + Sync,
-    T: crate::GraphQLTypeAsync<S>,
+    T: GraphQLTypeAsync<S> + ?Sized,
     T::TypeInfo: Send + Sync,
     T::Context: Send + Sync,
 {
@@ -142,8 +163,8 @@ where
         field_name: &'b str,
         arguments: &'b Arguments<S>,
         executor: &'b Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'b, ExecutionResult<S>> {
-        crate::GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor)
+    ) -> BoxFuture<'b, ExecutionResult<S>> {
+        GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor)
     }
 
     fn resolve_async<'a>(
@@ -151,8 +172,8 @@ where
         info: &'a Self::TypeInfo,
         selection_set: Option<&'a [Selection<S>]>,
         executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, ExecutionResult<S>> {
-        crate::GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor)
+    ) -> BoxFuture<'a, ExecutionResult<S>> {
+        GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor)
     }
 }
 
@@ -169,7 +190,7 @@ where
 impl<S, T> GraphQLType<S> for Arc<T>
 where
     S: ScalarValue,
-    T: GraphQLType<S>,
+    T: GraphQLType<S> + ?Sized,
 {
     type Context = T::Context;
     type TypeInfo = T::TypeInfo;
@@ -215,36 +236,19 @@ where
     }
 }
 
-impl<S, T, CtxT> crate::GraphQLTypeAsync<S> for Box<T>
-where
-    T: crate::GraphQLTypeAsync<S, Context = CtxT>,
-    T::TypeInfo: Send + Sync,
-    S: ScalarValue + Send + Sync,
-    CtxT: Send + Sync,
-{
-    fn resolve_async<'a>(
-        &'a self,
-        info: &'a Self::TypeInfo,
-        selection_set: Option<&'a [Selection<S>]>,
-        executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, crate::ExecutionResult<S>> {
-        (**self).resolve_async(info, selection_set, executor)
-    }
-}
-
-impl<'e, S, T> crate::GraphQLTypeAsync<S> for std::sync::Arc<T>
+impl<'e, S, T> GraphQLTypeAsync<S> for Arc<T>
 where
     S: ScalarValue + Send + Sync,
-    T: crate::GraphQLTypeAsync<S>,
-    <T as crate::types::base::GraphQLType<S>>::TypeInfo: Send + Sync,
-    <T as crate::types::base::GraphQLType<S>>::Context: Send + Sync,
+    T: GraphQLTypeAsync<S> + ?Sized,
+    <T as GraphQLType<S>>::TypeInfo: Send + Sync,
+    <T as GraphQLType<S>>::Context: Send + Sync,
 {
     fn resolve_async<'a>(
         &'a self,
         info: &'a Self::TypeInfo,
         selection_set: Option<&'a [Selection<S>]>,
         executor: &'a Executor<Self::Context, S>,
-    ) -> crate::BoxFuture<'a, crate::ExecutionResult<S>> {
+    ) -> BoxFuture<'a, ExecutionResult<S>> {
         (**self).resolve_async(info, selection_set, executor)
     }
 }
diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs
index 10d3d7f42..a2963c935 100644
--- a/juniper/src/types/scalars.rs
+++ b/juniper/src/types/scalars.rs
@@ -192,7 +192,7 @@ where
     })
 }
 
-impl<'a, S> GraphQLType<S> for &'a str
+impl<S> GraphQLType<S> for str
 where
     S: ScalarValue,
 {
@@ -216,11 +216,11 @@ where
         _: Option<&[Selection<S>]>,
         _: &Executor<Self::Context, S>,
     ) -> ExecutionResult<S> {
-        Ok(Value::scalar(String::from(*self)))
+        Ok(Value::scalar(String::from(self)))
     }
 }
 
-impl<'e, S> crate::GraphQLTypeAsync<S> for &'e str
+impl<S> crate::GraphQLTypeAsync<S> for str
 where
     S: ScalarValue + Send + Sync,
 {
@@ -325,11 +325,11 @@ where
 /// If you instantiate `RootNode` with this as the mutation, no mutation will be
 /// generated for the schema.
 #[derive(Debug)]
-pub struct EmptyMutation<T> {
+pub struct EmptyMutation<T: ?Sized = ()> {
     phantom: PhantomData<T>,
 }
 
-impl<T> EmptyMutation<T> {
+impl<T: ?Sized> EmptyMutation<T> {
     /// Construct a new empty mutation
     pub fn new() -> EmptyMutation<T> {
         EmptyMutation {
@@ -339,7 +339,7 @@ impl<T> EmptyMutation<T> {
 }
 
 // This is safe because `T` is never used.
-unsafe impl<T> Send for EmptyMutation<T> {}
+unsafe impl<T: ?Sized> Send for EmptyMutation<T> {}
 
 impl<S, T> GraphQLType<S> for EmptyMutation<T>
 where
@@ -382,14 +382,14 @@ impl<T> Default for EmptyMutation<T> {
 ///
 /// If you instantiate `RootNode` with this as the subscription,
 /// no subscriptions will be generated for the schema.
-pub struct EmptySubscription<T> {
+pub struct EmptySubscription<T: ?Sized = ()> {
     phantom: PhantomData<T>,
 }
 
 // This is safe due to never using `T`.
-unsafe impl<T> Send for EmptySubscription<T> {}
+unsafe impl<T: ?Sized> Send for EmptySubscription<T> {}
 
-impl<T> EmptySubscription<T> {
+impl<T: ?Sized> EmptySubscription<T> {
     /// Construct a new empty subscription
     pub fn new() -> Self {
         EmptySubscription {
diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs
index 5b6ccc56c..050769cd9 100644
--- a/juniper/src/types/subscriptions.rs
+++ b/juniper/src/types/subscriptions.rs
@@ -184,7 +184,7 @@ where
     'e: 'fut,
     'ref_e: 'fut,
     'res: 'fut,
-    T: GraphQLSubscriptionType<S, Context = CtxT>,
+    T: GraphQLSubscriptionType<S, Context = CtxT> + ?Sized,
     T::TypeInfo: Send + Sync,
     S: ScalarValue + Send + Sync,
     CtxT: Send + Sync,
@@ -203,7 +203,7 @@ async fn resolve_selection_set_into_stream_recursive<'i, 'inf, 'ref_e, 'e, 'res,
     executor: &'ref_e Executor<'ref_e, 'e, CtxT, S>,
 ) -> Value<ValuesStream<'res, S>>
 where
-    T: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync,
+    T: GraphQLSubscriptionType<S, Context = CtxT> + Send + Sync + ?Sized,
     T::TypeInfo: Send + Sync,
     S: ScalarValue + Send + Sync,
     CtxT: Send + Sync,
diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml
index be8639cd1..21faf7cca 100644
--- a/juniper_codegen/Cargo.toml
+++ b/juniper_codegen/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "juniper_codegen"
 version = "0.14.2"
+edition = "2018"
 authors = [
     "Magnus Hallin <mhallin@fastmail.com>",
     "Christoph Herzog <chris@theduke.at>",
@@ -9,20 +10,20 @@ description = "Internal custom derive trait for Juniper GraphQL"
 license = "BSD-2-Clause"
 documentation = "https://docs.rs/juniper"
 repository = "https://github.com/graphql-rust/juniper"
-edition = "2018"
+
+[badges]
+travis-ci = { repository = "graphql-rust/juniper" }
 
 [lib]
 proc-macro = true
 
 [dependencies]
+proc-macro-error = "1.0.2"
 proc-macro2 = "1.0.1"
-syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
 quote = "1.0.3"
-futures = "0.3.1"
-proc-macro-error = "1.0.2"
+syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
 
 [dev-dependencies]
-juniper = { version = "0.14.2", path = "../juniper"}
-
-[badges]
-travis-ci = { repository = "graphql-rust/juniper" }
+derive_more = "0.99.7"
+futures = "0.3.1"
+juniper = { version = "0.14.2", path = "../juniper" }
diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs
index 9dc9b7dce..b1a7e4cd1 100644
--- a/juniper_codegen/src/derive_enum.rs
+++ b/juniper_codegen/src/derive_enum.rs
@@ -58,7 +58,7 @@ pub fn impl_enum(
             let _type = match field.fields {
                 Fields::Unit => syn::parse_str(&field_name.to_string()).unwrap(),
                 _ => {
-                    error.custom(
+                    error.emit_custom(
                         field.fields.span(),
                         "all fields of the enum must be unnamed, e.g., None",
                     );
@@ -145,6 +145,7 @@ pub fn impl_enum(
         include_type_generics: true,
         generic_scalar: true,
         no_async: attrs.no_async.is_some(),
+        mode: is_internal.into(),
     };
 
     let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs
index 843bfb2da..a85cd4d85 100644
--- a/juniper_codegen/src/derive_input_object.rs
+++ b/juniper_codegen/src/derive_input_object.rs
@@ -145,6 +145,7 @@ pub fn impl_input_object(
         include_type_generics: true,
         generic_scalar: true,
         no_async: attrs.no_async.is_some(),
+        mode: is_internal.into(),
     };
 
     let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs
index 04dc4556c..ad5b7cc71 100644
--- a/juniper_codegen/src/derive_object.rs
+++ b/juniper_codegen/src/derive_object.rs
@@ -132,6 +132,7 @@ pub fn build_derive_object(
         include_type_generics: true,
         generic_scalar: true,
         no_async: attrs.no_async.is_some(),
+        mode: is_internal.into(),
     };
 
     let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs
index 6474324b6..c951b1e5c 100644
--- a/juniper_codegen/src/derive_scalar_value.rs
+++ b/juniper_codegen/src/derive_scalar_value.rs
@@ -127,7 +127,7 @@ fn impl_scalar_struct(
                 executor: &'a #crate_name::Executor<Self::Context, __S>,
             ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<__S>> {
                 use #crate_name::GraphQLType;
-                use futures::future;
+                use #crate_name::futures::future;
                 let v = self.resolve(info, selection_set, executor);
                 Box::pin(future::ready(v))
             }
diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs
deleted file mode 100644
index 0b94304e2..000000000
--- a/juniper_codegen/src/derive_union.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use crate::{
-    result::{GraphQLScope, UnsupportedAttribute},
-    util::{self, span_container::SpanContainer},
-};
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
-
-pub fn build_derive_union(
-    ast: syn::DeriveInput,
-    is_internal: bool,
-    error: GraphQLScope,
-) -> syn::Result<TokenStream> {
-    let ast_span = ast.span();
-    let enum_fields = match ast.data {
-        Data::Enum(data) => data.variants,
-        _ => return Err(error.custom_error(ast_span, "can only be applied to enums")),
-    };
-
-    // Parse attributes.
-    let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
-
-    let ident = &ast.ident;
-    let name = attrs
-        .name
-        .clone()
-        .map(SpanContainer::into_inner)
-        .unwrap_or_else(|| ident.unraw().to_string());
-
-    let fields = enum_fields
-        .into_iter()
-        .filter_map(|field| {
-            let span = field.span();
-            let field_attrs = match util::FieldAttributes::from_attrs(
-                &field.attrs,
-                util::FieldAttributeParseMode::Object,
-            ) {
-                Ok(attrs) => attrs,
-                Err(e) => {
-                    proc_macro_error::emit_error!(e);
-                    return None;
-                }
-            };
-
-            if let Some(ident) = field_attrs.skip {
-                error.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip);
-                return None;
-            }
-
-            let variant_name = field.ident;
-            let name = field_attrs
-                .name
-                .clone()
-                .map(SpanContainer::into_inner)
-                .unwrap_or_else(|| util::to_camel_case(&variant_name.unraw().to_string()));
-
-            let resolver_code = quote!(
-                #ident :: #variant_name
-            );
-
-            let _type = match field.fields {
-                Fields::Unnamed(inner) => {
-                    let mut iter = inner.unnamed.iter();
-                    let first = match iter.next() {
-                        Some(val) => val,
-                        None => unreachable!(),
-                    };
-
-                    if iter.next().is_some() {
-                        error.custom(
-                            inner.span(),
-                            "all members must be unnamed with a single element e.g. Some(T)",
-                        );
-                    }
-
-                    first.ty.clone()
-                }
-                _ => {
-                    error.custom(
-                        variant_name.span(),
-                        "only unnamed fields with a single element are allowed, e.g., Some(T)",
-                    );
-
-                    return None;
-                }
-            };
-
-            if let Some(description) = field_attrs.description {
-                error.unsupported_attribute_within(
-                    description.span_ident(),
-                    UnsupportedAttribute::Description,
-                );
-            }
-
-            if let Some(default) = field_attrs.default {
-                error.unsupported_attribute_within(
-                    default.span_ident(),
-                    UnsupportedAttribute::Default,
-                );
-            }
-
-            if name.starts_with("__") {
-                error.no_double_underscore(if let Some(name) = field_attrs.name {
-                    name.span_ident()
-                } else {
-                    variant_name.span()
-                });
-            }
-
-            Some(util::GraphQLTypeDefinitionField {
-                name,
-                _type,
-                args: Vec::new(),
-                description: None,
-                deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
-                resolver_code,
-                is_type_inferred: true,
-                is_async: false,
-                default: None,
-                span,
-            })
-        })
-        .collect::<Vec<_>>();
-
-    // Early abort after checking all fields
-    proc_macro_error::abort_if_dirty();
-
-    if !attrs.interfaces.is_empty() {
-        attrs.interfaces.iter().for_each(|elm| {
-            error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
-        });
-    }
-
-    if fields.is_empty() {
-        error.not_empty(ast_span);
-    }
-
-    if name.starts_with("__") && !is_internal {
-        error.no_double_underscore(if let Some(name) = attrs.name {
-            name.span_ident()
-        } else {
-            ident.span()
-        });
-    }
-
-    // NOTICE: This is not an optimal implementation. It is possible
-    // to bypass this check by using a full qualified path instead
-    // (crate::Test vs Test). Since this requirement is mandatory, the
-    // `std::convert::Into<T>` implementation is used to enforce this
-    // requirement. However, due to the bad error message this
-    // implementation should stay and provide guidance.
-    let all_variants_different = {
-        let mut all_types: Vec<_> = fields.iter().map(|field| &field._type).collect();
-        let before = all_types.len();
-        all_types.dedup();
-        before == all_types.len()
-    };
-
-    if !all_variants_different {
-        error.custom(ident.span(), "each variant must have a different type");
-    }
-
-    // Early abort after GraphQL properties
-    proc_macro_error::abort_if_dirty();
-
-    let definition = util::GraphQLTypeDefiniton {
-        name,
-        _type: syn::parse_str(&ast.ident.to_string()).unwrap(),
-        context: attrs.context.map(SpanContainer::into_inner),
-        scalar: attrs.scalar.map(SpanContainer::into_inner),
-        description: attrs.description.map(SpanContainer::into_inner),
-        fields,
-        generics: ast.generics,
-        interfaces: None,
-        include_type_generics: true,
-        generic_scalar: true,
-        no_async: attrs.no_async.is_some(),
-    };
-
-    let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
-    Ok(definition.into_union_tokens(juniper_crate_name))
-}
diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs
new file mode 100644
index 000000000..d8561f699
--- /dev/null
+++ b/juniper_codegen/src/graphql_union/attr.rs
@@ -0,0 +1,345 @@
+//! Code generation for `#[graphql_union]`/`#[graphql_union_internal]` macros.
+
+use std::{mem, ops::Deref as _};
+
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, ToTokens as _};
+use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _};
+
+use crate::{
+    result::GraphQLScope,
+    util::{path_eq_single, span_container::SpanContainer, unparenthesize, Mode},
+};
+
+use super::{
+    all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta,
+    UnionVariantDefinition, UnionVariantMeta,
+};
+
+/// [`GraphQLScope`] of errors for `#[graphql_union]`/`#[graphql_union_internal]` macros.
+const ERR: GraphQLScope = GraphQLScope::UnionAttr;
+
+/// Returns the concrete name of the `proc_macro_attribute` for deriving `GraphQLUnion`
+/// implementation depending on the provided `mode`.
+fn attr_path(mode: Mode) -> &'static str {
+    match mode {
+        Mode::Public => "graphql_union",
+        Mode::Internal => "graphql_union_internal",
+    }
+}
+
+/// Expands `#[graphql_union]`/`#[graphql_union_internal]` macro into generated code.
+pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Result<TokenStream> {
+    let attr_path = attr_path(mode);
+
+    let mut ast = syn::parse2::<syn::ItemTrait>(body).map_err(|_| {
+        syn::Error::new(
+            Span::call_site(),
+            format!(
+                "#[{}] attribute is applicable to trait definitions only",
+                attr_path,
+            ),
+        )
+    })?;
+
+    let mut trait_attrs = Vec::with_capacity(ast.attrs.len() + 1);
+    trait_attrs.push({
+        let attr_path = syn::Ident::new(attr_path, Span::call_site());
+        parse_quote! { #[#attr_path(#attr_args)] }
+    });
+    trait_attrs.extend_from_slice(&ast.attrs);
+
+    // Remove repeated attributes from the definition, to omit duplicate expansion.
+    ast.attrs = ast
+        .attrs
+        .into_iter()
+        .filter_map(|attr| {
+            if path_eq_single(&attr.path, attr_path) {
+                None
+            } else {
+                Some(attr)
+            }
+        })
+        .collect();
+
+    let meta = UnionMeta::from_attrs(attr_path, &trait_attrs)?;
+
+    let trait_span = ast.span();
+    let trait_ident = &ast.ident;
+
+    let name = meta
+        .name
+        .clone()
+        .map(SpanContainer::into_inner)
+        .unwrap_or_else(|| trait_ident.unraw().to_string());
+    if matches!(mode, Mode::Public) && name.starts_with("__") {
+        ERR.no_double_underscore(
+            meta.name
+                .as_ref()
+                .map(SpanContainer::span_ident)
+                .unwrap_or_else(|| trait_ident.span()),
+        );
+    }
+
+    let mut variants: Vec<_> = ast
+        .items
+        .iter_mut()
+        .filter_map(|i| match i {
+            syn::TraitItem::Method(m) => {
+                parse_variant_from_trait_method(m, trait_ident, &meta, mode)
+            }
+            _ => None,
+        })
+        .collect();
+
+    proc_macro_error::abort_if_dirty();
+
+    emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode);
+
+    if variants.is_empty() {
+        ERR.emit_custom(trait_span, "expects at least one union variant");
+    }
+
+    if !all_variants_different(&variants) {
+        ERR.emit_custom(
+            trait_span,
+            "must have a different type for each union variant",
+        );
+    }
+
+    proc_macro_error::abort_if_dirty();
+
+    let context = meta
+        .context
+        .map(SpanContainer::into_inner)
+        .or_else(|| variants.iter().find_map(|v| v.context_ty.as_ref()).cloned());
+
+    let generated_code = UnionDefinition {
+        name,
+        ty: parse_quote! { #trait_ident },
+        is_trait_object: true,
+        description: meta.description.map(SpanContainer::into_inner),
+        context,
+        scalar: meta.scalar.map(SpanContainer::into_inner),
+        generics: ast.generics.clone(),
+        variants,
+        span: trait_span,
+        mode,
+    };
+
+    Ok(quote! {
+        #ast
+
+        #generated_code
+    })
+}
+
+/// Parses given Rust trait `method` as [GraphQL union][1] variant.
+///
+/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding
+/// errors.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+fn parse_variant_from_trait_method(
+    method: &mut syn::TraitItemMethod,
+    trait_ident: &syn::Ident,
+    trait_meta: &UnionMeta,
+    mode: Mode,
+) -> Option<UnionVariantDefinition> {
+    let attr_path = attr_path(mode);
+    let method_attrs = method.attrs.clone();
+
+    // Remove repeated attributes from the method, to omit incorrect expansion.
+    method.attrs = mem::take(&mut method.attrs)
+        .into_iter()
+        .filter_map(|attr| {
+            if path_eq_single(&attr.path, attr_path) {
+                None
+            } else {
+                Some(attr)
+            }
+        })
+        .collect();
+
+    let meta = UnionVariantMeta::from_attrs(attr_path, &method_attrs)
+        .map_err(|e| proc_macro_error::emit_error!(e))
+        .ok()?;
+
+    if let Some(rslvr) = meta.external_resolver {
+        ERR.custom(
+            rslvr.span_ident(),
+            format!(
+                "cannot use #[{}(with = ...)] attribute on a trait method",
+                attr_path,
+            ),
+        )
+        .note(format!(
+            "instead use #[{0}(ignore)] on the method with #[{0}(on ... = ...)] on the trait \
+             itself",
+            attr_path,
+        ))
+        .emit()
+    }
+    if meta.ignore.is_some() {
+        return None;
+    }
+
+    let method_span = method.sig.span();
+    let method_ident = &method.sig.ident;
+
+    let ty = parse_trait_method_output_type(&method.sig)
+        .map_err(|span| {
+            ERR.emit_custom(
+                span,
+                "expects trait method return type to be `Option<&VariantType>` only",
+            )
+        })
+        .ok()?;
+    let method_context_ty = parse_trait_method_input_args(&method.sig)
+        .map_err(|span| {
+            ERR.emit_custom(
+                span,
+                "expects trait method to accept `&self` only and, optionally, `&Context`",
+            )
+        })
+        .ok()?;
+    if let Some(is_async) = &method.sig.asyncness {
+        ERR.emit_custom(
+            is_async.span(),
+            "doesn't support async union variants resolvers yet",
+        );
+        return None;
+    }
+
+    let resolver_code = {
+        if let Some(other) = trait_meta.external_resolvers.get(&ty) {
+            ERR.custom(
+                method_span,
+                format!(
+                    "trait method `{}` conflicts with the external resolver function `{}` declared \
+                     on the trait to resolve the variant type `{}`",
+                    method_ident,
+                    other.to_token_stream(),
+                    ty.to_token_stream(),
+
+                ),
+            )
+            .note(format!(
+                "use `#[{}(ignore)]` attribute to ignore this trait method for union variants \
+                 resolution",
+                attr_path,
+            ))
+            .emit();
+        }
+
+        if method_context_ty.is_some() {
+            let crate_path = mode.crate_path();
+
+            parse_quote! {
+                #trait_ident::#method_ident(self, #crate_path::FromContext::from(context))
+            }
+        } else {
+            parse_quote! {
+                #trait_ident::#method_ident(self)
+            }
+        }
+    };
+
+    // Doing this may be quite an expensive, because resolving may contain some heavy computation,
+    // so we're preforming it twice. Unfortunately, we have no other options here, until the
+    // `juniper::GraphQLType` itself will allow to do it in some cleverer way.
+    let resolver_check = parse_quote! {
+        ({ #resolver_code } as ::std::option::Option<&#ty>).is_some()
+    };
+
+    Some(UnionVariantDefinition {
+        ty,
+        resolver_code,
+        resolver_check,
+        enum_path: None,
+        context_ty: method_context_ty,
+        span: method_span,
+    })
+}
+
+/// Parses type of [GraphQL union][1] variant from the return type of trait method.
+///
+/// If return type is invalid, then returns the [`Span`] to display the corresponding error at.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+fn parse_trait_method_output_type(sig: &syn::Signature) -> Result<syn::Type, Span> {
+    let ret_ty = match &sig.output {
+        syn::ReturnType::Type(_, ty) => ty.deref(),
+        _ => return Err(sig.span()),
+    };
+
+    let path = match unparenthesize(ret_ty) {
+        syn::Type::Path(syn::TypePath { qself: None, path }) => path,
+        _ => return Err(ret_ty.span()),
+    };
+
+    let (ident, args) = match path.segments.last() {
+        Some(syn::PathSegment {
+            ident,
+            arguments: syn::PathArguments::AngleBracketed(generic),
+        }) => (ident, &generic.args),
+        _ => return Err(ret_ty.span()),
+    };
+
+    if ident.unraw() != "Option" {
+        return Err(ret_ty.span());
+    }
+
+    if args.len() != 1 {
+        return Err(ret_ty.span());
+    }
+    let var_ty = match args.first() {
+        Some(syn::GenericArgument::Type(inner_ty)) => match unparenthesize(inner_ty) {
+            syn::Type::Reference(inner_ty) => {
+                if inner_ty.mutability.is_some() {
+                    return Err(inner_ty.span());
+                }
+                unparenthesize(inner_ty.elem.deref()).clone()
+            }
+            _ => return Err(ret_ty.span()),
+        },
+        _ => return Err(ret_ty.span()),
+    };
+    Ok(var_ty)
+}
+
+/// Parses trait method input arguments and validates them to be acceptable for resolving into
+/// [GraphQL union][1] variant type. Returns type of the context used in input arguments, if any.
+///
+/// If input arguments are invalid, then returns the [`Span`] to display the corresponding error at.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+fn parse_trait_method_input_args(sig: &syn::Signature) -> Result<Option<syn::Type>, Span> {
+    match sig.receiver() {
+        Some(syn::FnArg::Receiver(rcv)) => {
+            if rcv.reference.is_none() || rcv.mutability.is_some() {
+                return Err(rcv.span());
+            }
+        }
+        _ => return Err(sig.span()),
+    }
+
+    if sig.inputs.len() > 2 {
+        return Err(sig.inputs.span());
+    }
+
+    let second_arg_ty = match sig.inputs.iter().skip(1).next() {
+        Some(syn::FnArg::Typed(arg)) => arg.ty.deref(),
+        None => return Ok(None),
+        _ => return Err(sig.inputs.span()),
+    };
+    match unparenthesize(second_arg_ty) {
+        syn::Type::Reference(ref_ty) => {
+            if ref_ty.mutability.is_some() {
+                return Err(ref_ty.span());
+            }
+            Ok(Some(ref_ty.elem.deref().clone()))
+        }
+        ty => Err(ty.span()),
+    }
+}
diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs
new file mode 100644
index 000000000..8f0f7ed56
--- /dev/null
+++ b/juniper_codegen/src/graphql_union/derive.rs
@@ -0,0 +1,226 @@
+//! Code generation for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros.
+
+use proc_macro2::TokenStream;
+use proc_macro_error::ResultExt as _;
+use quote::{quote, ToTokens};
+use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields};
+
+use crate::{
+    result::GraphQLScope,
+    util::{span_container::SpanContainer, unparenthesize, Mode},
+};
+
+use super::{
+    all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta,
+    UnionVariantDefinition, UnionVariantMeta,
+};
+
+/// [`GraphQLScope`] of errors for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]`
+/// macros.
+const ERR: GraphQLScope = GraphQLScope::UnionDerive;
+
+/// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro into generated code.
+pub fn expand(input: TokenStream, mode: Mode) -> syn::Result<TokenStream> {
+    let ast = syn::parse2::<syn::DeriveInput>(input).unwrap_or_abort();
+
+    match &ast.data {
+        Data::Enum(_) => expand_enum(ast, mode),
+        Data::Struct(_) => expand_struct(ast, mode),
+        _ => Err(ERR.custom_error(ast.span(), "can only be derived for enums and structs")),
+    }
+    .map(ToTokens::into_token_stream)
+}
+
+/// Expands into generated code a `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro
+/// placed on a Rust enum.
+fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result<UnionDefinition> {
+    let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?;
+
+    let enum_span = ast.span();
+    let enum_ident = ast.ident;
+
+    let name = meta
+        .name
+        .clone()
+        .map(SpanContainer::into_inner)
+        .unwrap_or_else(|| enum_ident.unraw().to_string());
+    if matches!(mode, Mode::Public) && name.starts_with("__") {
+        ERR.no_double_underscore(
+            meta.name
+                .as_ref()
+                .map(SpanContainer::span_ident)
+                .unwrap_or_else(|| enum_ident.span()),
+        );
+    }
+
+    let mut variants: Vec<_> = match ast.data {
+        Data::Enum(data) => data.variants,
+        _ => unreachable!(),
+    }
+    .into_iter()
+    .filter_map(|var| parse_variant_from_enum_variant(var, &enum_ident, &meta, mode))
+    .collect();
+
+    proc_macro_error::abort_if_dirty();
+
+    emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode);
+
+    if variants.is_empty() {
+        ERR.emit_custom(enum_span, "expects at least one union variant");
+    }
+
+    if !all_variants_different(&variants) {
+        ERR.emit_custom(
+            enum_span,
+            "must have a different type for each union variant",
+        );
+    }
+
+    proc_macro_error::abort_if_dirty();
+
+    Ok(UnionDefinition {
+        name,
+        ty: parse_quote! { #enum_ident },
+        is_trait_object: false,
+        description: meta.description.map(SpanContainer::into_inner),
+        context: meta.context.map(SpanContainer::into_inner),
+        scalar: meta.scalar.map(SpanContainer::into_inner),
+        generics: ast.generics,
+        variants,
+        span: enum_span,
+        mode,
+    })
+}
+
+/// Parses given Rust enum `var`iant as [GraphQL union][1] variant.
+///
+/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding
+/// errors.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+fn parse_variant_from_enum_variant(
+    var: syn::Variant,
+    enum_ident: &syn::Ident,
+    enum_meta: &UnionMeta,
+    mode: Mode,
+) -> Option<UnionVariantDefinition> {
+    let meta = UnionVariantMeta::from_attrs("graphql", &var.attrs)
+        .map_err(|e| proc_macro_error::emit_error!(e))
+        .ok()?;
+    if meta.ignore.is_some() {
+        return None;
+    }
+
+    let var_span = var.span();
+    let var_ident = var.ident;
+
+    let ty = match var.fields {
+        Fields::Unnamed(fields) => {
+            let mut iter = fields.unnamed.iter();
+            let first = iter.next().unwrap();
+            if iter.next().is_none() {
+                Ok(unparenthesize(&first.ty).clone())
+            } else {
+                Err(fields.span())
+            }
+        }
+        _ => Err(var_ident.span()),
+    }
+    .map_err(|span| {
+        ERR.emit_custom(
+            span,
+            "enum allows only unnamed variants with a single field, e.g. `Some(T)`",
+        )
+    })
+    .ok()?;
+
+    let enum_path = quote! { #enum_ident::#var_ident };
+
+    let resolver_code = if let Some(rslvr) = meta.external_resolver {
+        if let Some(other) = enum_meta.external_resolvers.get(&ty) {
+            ERR.emit_custom(
+                rslvr.span_ident(),
+                format!(
+                    "variant `{}` already has external resolver function `{}` declared on the enum",
+                    ty.to_token_stream(),
+                    other.to_token_stream(),
+                ),
+            );
+        }
+
+        let crate_path = mode.crate_path();
+        let resolver_fn = rslvr.into_inner();
+
+        parse_quote! {
+            #resolver_fn(self, #crate_path::FromContext::from(context))
+        }
+    } else {
+        parse_quote! {
+            match self { #enum_ident::#var_ident(ref v) => Some(v), _ => None, }
+        }
+    };
+
+    let resolver_check = parse_quote! {
+        matches!(self, #enum_path(_))
+    };
+
+    Some(UnionVariantDefinition {
+        ty,
+        resolver_code,
+        resolver_check,
+        enum_path: Some(enum_path),
+        context_ty: None,
+        span: var_span,
+    })
+}
+
+/// Expands into generated code a `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro
+/// placed on a Rust struct.
+fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result<UnionDefinition> {
+    let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?;
+
+    let struct_span = ast.span();
+    let struct_ident = ast.ident;
+
+    let name = meta
+        .name
+        .clone()
+        .map(SpanContainer::into_inner)
+        .unwrap_or_else(|| struct_ident.unraw().to_string());
+    if matches!(mode, Mode::Public) && name.starts_with("__") {
+        ERR.no_double_underscore(
+            meta.name
+                .as_ref()
+                .map(SpanContainer::span_ident)
+                .unwrap_or_else(|| struct_ident.span()),
+        );
+    }
+
+    let mut variants = vec![];
+    emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode);
+    if variants.is_empty() {
+        ERR.emit_custom(struct_span, "expects at least one union variant");
+    }
+
+    if !all_variants_different(&variants) {
+        ERR.emit_custom(
+            struct_span,
+            "must have a different type for each union variant",
+        );
+    }
+
+    proc_macro_error::abort_if_dirty();
+
+    Ok(UnionDefinition {
+        name,
+        ty: parse_quote! { #struct_ident },
+        is_trait_object: false,
+        description: meta.description.map(SpanContainer::into_inner),
+        context: meta.context.map(SpanContainer::into_inner),
+        scalar: meta.scalar.map(SpanContainer::into_inner),
+        generics: ast.generics,
+        variants,
+        span: struct_span,
+        mode,
+    })
+}
diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs
new file mode 100644
index 000000000..2d8132c28
--- /dev/null
+++ b/juniper_codegen/src/graphql_union/mod.rs
@@ -0,0 +1,700 @@
+//! Code generation for [GraphQL union][1].
+//!
+//! [1]: https://spec.graphql.org/June2018/#sec-Unions
+
+pub mod attr;
+pub mod derive;
+
+use std::collections::HashMap;
+
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, ToTokens, TokenStreamExt as _};
+use syn::{
+    parse::{Parse, ParseStream},
+    parse_quote,
+    spanned::Spanned as _,
+};
+
+use crate::util::{
+    filter_attrs, get_doc_comment, span_container::SpanContainer, Mode, OptionExt as _,
+};
+
+/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of
+/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related
+/// to the `$another` struct (a later one).
+///
+/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
+/// By default, [`SpanContainer::span_ident`] is used.
+macro_rules! try_merge_opt {
+    ($field:ident: $self:ident, $another:ident => $span:ident) => {{
+        if let Some(v) = $self.$field {
+            $another
+                .$field
+                .replace(v)
+                .none_or_else(|dup| dup_attr_err(dup.$span()))?;
+        }
+        $another.$field
+    }};
+
+    ($field:ident: $self:ident, $another:ident) => {
+        try_merge_opt!($field: $self, $another => span_ident)
+    };
+}
+
+/// Attempts to merge a [`HashMap`]ed `$field` of a `$self` struct with the same `$field` of
+/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error
+/// with a [`Span`] related to the `$another` struct (a later one).
+///
+/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods.
+/// By default, [`SpanContainer::span_ident`] is used.
+macro_rules! try_merge_hashmap {
+    ($field:ident: $self:ident, $another:ident => $span:ident) => {{
+        if !$self.$field.is_empty() {
+            for (ty, rslvr) in $self.$field {
+                $another
+                    .$field
+                    .insert(ty, rslvr)
+                    .none_or_else(|dup| dup_attr_err(dup.$span()))?;
+            }
+        }
+        $another.$field
+    }};
+
+    ($field:ident: $self:ident, $another:ident) => {
+        try_merge_hashmap!($field: $self, $another => span_ident)
+    };
+}
+
+/// Creates and returns duplication error pointing to the given `span`.
+fn dup_attr_err(span: Span) -> syn::Error {
+    syn::Error::new(span, "duplicated attribute")
+}
+
+/// Helper alias for the type of [`UnionMeta::external_resolvers`] field.
+type UnionMetaResolvers = HashMap<syn::Type, SpanContainer<syn::ExprPath>>;
+
+/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when
+/// generating code for [GraphQL union][1] type.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+#[derive(Debug, Default)]
+struct UnionMeta {
+    /// Explicitly specified name of [GraphQL union][1] type.
+    ///
+    /// If absent, then Rust type name is used by default.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub name: Option<SpanContainer<String>>,
+
+    /// Explicitly specified [description][2] of [GraphQL union][1] type.
+    ///
+    /// If absent, then Rust doc comment is used as [description][2], if any.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions
+    pub description: Option<SpanContainer<String>>,
+
+    /// Explicitly specified type of `juniper::Context` to use for resolving this [GraphQL union][1]
+    /// type with.
+    ///
+    /// If absent, then unit type `()` is assumed as type of `juniper::Context`.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub context: Option<SpanContainer<syn::Type>>,
+
+    /// Explicitly specified type of `juniper::ScalarValue` to use for resolving this
+    /// [GraphQL union][1] type with.
+    ///
+    /// If absent, then generated code will be generic over any `juniper::ScalarValue` type, which,
+    /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type
+    /// too. That's why this type should be specified only if one of the variants implements
+    /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub scalar: Option<SpanContainer<syn::Type>>,
+
+    /// Explicitly specified external resolver functions for [GraphQL union][1] variants.
+    ///
+    /// If absent, then macro will try to auto-infer all the possible variants from the type
+    /// declaration, if possible. That's why specifying an external resolver function has sense,
+    /// when some custom [union][1] variant resolving logic is involved, or variants cannot be
+    /// inferred.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub external_resolvers: UnionMetaResolvers,
+}
+
+impl Parse for UnionMeta {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut output = Self::default();
+
+        while !input.is_empty() {
+            let ident: syn::Ident = input.parse()?;
+            match ident.to_string().as_str() {
+                "name" => {
+                    input.parse::<syn::Token![=]>()?;
+                    let name = input.parse::<syn::LitStr>()?;
+                    output
+                        .name
+                        .replace(SpanContainer::new(
+                            ident.span(),
+                            Some(name.span()),
+                            name.value(),
+                        ))
+                        .none_or_else(|_| dup_attr_err(ident.span()))?
+                }
+                "desc" | "description" => {
+                    input.parse::<syn::Token![=]>()?;
+                    let desc = input.parse::<syn::LitStr>()?;
+                    output
+                        .description
+                        .replace(SpanContainer::new(
+                            ident.span(),
+                            Some(desc.span()),
+                            desc.value(),
+                        ))
+                        .none_or_else(|_| dup_attr_err(ident.span()))?
+                }
+                "ctx" | "context" | "Context" => {
+                    input.parse::<syn::Token![=]>()?;
+                    let ctx = input.parse::<syn::Type>()?;
+                    output
+                        .context
+                        .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx))
+                        .none_or_else(|_| dup_attr_err(ident.span()))?
+                }
+                "scalar" | "Scalar" | "ScalarValue" => {
+                    input.parse::<syn::Token![=]>()?;
+                    let scl = input.parse::<syn::Type>()?;
+                    output
+                        .scalar
+                        .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl))
+                        .none_or_else(|_| dup_attr_err(ident.span()))?
+                }
+                "on" => {
+                    let ty = input.parse::<syn::Type>()?;
+                    input.parse::<syn::Token![=]>()?;
+                    let rslvr = input.parse::<syn::ExprPath>()?;
+                    let rslvr_spanned = SpanContainer::new(ident.span(), Some(ty.span()), rslvr);
+                    let rslvr_span = rslvr_spanned.span_joined();
+                    output
+                        .external_resolvers
+                        .insert(ty, rslvr_spanned)
+                        .none_or_else(|_| dup_attr_err(rslvr_span))?
+                }
+                _ => {
+                    return Err(syn::Error::new(ident.span(), "unknown attribute"));
+                }
+            }
+            if input.lookahead1().peek(syn::Token![,]) {
+                input.parse::<syn::Token![,]>()?;
+            }
+        }
+
+        Ok(output)
+    }
+}
+
+impl UnionMeta {
+    /// Tries to merge two [`UnionMeta`]s into single one, reporting about duplicates, if any.
+    fn try_merge(self, mut another: Self) -> syn::Result<Self> {
+        Ok(Self {
+            name: try_merge_opt!(name: self, another),
+            description: try_merge_opt!(description: self, another),
+            context: try_merge_opt!(context: self, another),
+            scalar: try_merge_opt!(scalar: self, another),
+            external_resolvers: try_merge_hashmap!(external_resolvers: self, another => span_joined),
+        })
+    }
+
+    /// Parses [`UnionMeta`] from the given multiple `name`d attributes placed on type definition.
+    pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
+        let mut meta = filter_attrs(name, attrs)
+            .map(|attr| attr.parse_args())
+            .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?;
+
+        if meta.description.is_none() {
+            meta.description = get_doc_comment(attrs);
+        }
+
+        Ok(meta)
+    }
+}
+
+/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when
+/// generating code for [GraphQL union][1]'s variant.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+#[derive(Debug, Default)]
+struct UnionVariantMeta {
+    /// Explicitly specified marker for the variant/field being ignored and not included into
+    /// [GraphQL union][1].
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub ignore: Option<SpanContainer<syn::Ident>>,
+
+    /// Explicitly specified external resolver function for this [GraphQL union][1] variant.
+    ///
+    /// If absent, then macro will generate the code which just returns the variant inner value.
+    /// Usually, specifying an external resolver function has sense, when some custom resolving
+    /// logic is involved.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub external_resolver: Option<SpanContainer<syn::ExprPath>>,
+}
+
+impl Parse for UnionVariantMeta {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut output = Self::default();
+
+        while !input.is_empty() {
+            let ident: syn::Ident = input.parse()?;
+            match ident.to_string().as_str() {
+                "ignore" | "skip" => output
+                    .ignore
+                    .replace(SpanContainer::new(ident.span(), None, ident.clone()))
+                    .none_or_else(|_| dup_attr_err(ident.span()))?,
+                "with" => {
+                    input.parse::<syn::Token![=]>()?;
+                    let rslvr = input.parse::<syn::ExprPath>()?;
+                    output
+                        .external_resolver
+                        .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr))
+                        .none_or_else(|_| dup_attr_err(ident.span()))?
+                }
+                _ => {
+                    return Err(syn::Error::new(ident.span(), "unknown attribute"));
+                }
+            }
+            if input.lookahead1().peek(syn::Token![,]) {
+                input.parse::<syn::Token![,]>()?;
+            }
+        }
+
+        Ok(output)
+    }
+}
+
+impl UnionVariantMeta {
+    /// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if
+    /// any.
+    fn try_merge(self, mut another: Self) -> syn::Result<Self> {
+        Ok(Self {
+            ignore: try_merge_opt!(ignore: self, another),
+            external_resolver: try_merge_opt!(external_resolver: self, another),
+        })
+    }
+
+    /// Parses [`UnionVariantMeta`] from the given multiple `name`d attributes placed on
+    /// variant/field/method definition.
+    pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> {
+        filter_attrs(name, attrs)
+            .map(|attr| attr.parse_args())
+            .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))
+    }
+}
+
+/// Definition of [GraphQL union][1] variant for code generation.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+struct UnionVariantDefinition {
+    /// Rust type that this [GraphQL union][1] variant resolves into.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub ty: syn::Type,
+
+    /// Rust code for value resolution of this [GraphQL union][1] variant.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub resolver_code: syn::Expr,
+
+    /// Rust code for checking whether [GraphQL union][1] should be resolved into this variant.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub resolver_check: syn::Expr,
+
+    /// Rust enum variant path that this [GraphQL union][1] variant is associated with.
+    ///
+    /// It's available only when code generation happens for Rust enums.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub enum_path: Option<TokenStream>,
+
+    /// Rust type of `juniper::Context` that this [GraphQL union][1] variant requires for
+    /// resolution.
+    ///
+    /// It's available only when code generation happens for Rust traits and a trait method contains
+    /// context argument.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub context_ty: Option<syn::Type>,
+
+    /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1] variant.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub span: Span,
+}
+
+/// Definition of [GraphQL union][1] for code generation.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+struct UnionDefinition {
+    /// Name of this [GraphQL union][1] in GraphQL schema.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub name: String,
+
+    /// Rust type that this [GraphQL union][1] is represented with.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub ty: syn::Type,
+
+    /// Generics of the Rust type that this [GraphQL union][1] is implemented for.
+    pub generics: syn::Generics,
+
+    /// Indicator whether code should be generated for a trait object, rather than for a regular
+    /// Rust type.
+    pub is_trait_object: bool,
+
+    /// Description of this [GraphQL union][1] to put into GraphQL schema.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub description: Option<String>,
+
+    /// Rust type of `juniper::Context` to generate `juniper::GraphQLType` implementation with
+    /// for this [GraphQL union][1].
+    ///
+    /// If [`None`] then generated code will use unit type `()` as `juniper::Context`.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub context: Option<syn::Type>,
+
+    /// Rust type of `juniper::ScalarValue` to generate `juniper::GraphQLType` implementation with
+    /// for this [GraphQL union][1].
+    ///
+    /// If [`None`] then generated code will be generic over any `juniper::ScalarValue` type, which,
+    /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type
+    /// too. That's why this type should be specified only if one of the variants implements
+    /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type.
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub scalar: Option<syn::Type>,
+
+    /// Variants definitions of this [GraphQL union][1].
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub variants: Vec<UnionVariantDefinition>,
+
+    /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1].
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub span: Span,
+
+    /// [`Mode`] to generate code in for this [GraphQL union][1].
+    ///
+    /// [1]: https://spec.graphql.org/June2018/#sec-Unions
+    pub mode: Mode,
+}
+
+impl ToTokens for UnionDefinition {
+    fn to_tokens(&self, into: &mut TokenStream) {
+        let crate_path = self.mode.crate_path();
+
+        let name = &self.name;
+        let ty = &self.ty;
+
+        let context = self
+            .context
+            .as_ref()
+            .map(|ctx| quote! { #ctx })
+            .unwrap_or_else(|| quote! { () });
+
+        let scalar = self
+            .scalar
+            .as_ref()
+            .map(|scl| quote! { #scl })
+            .unwrap_or_else(|| quote! { __S });
+        let default_scalar = self
+            .scalar
+            .as_ref()
+            .map(|scl| quote! { #scl })
+            .unwrap_or_else(|| quote! { #crate_path::DefaultScalarValue });
+
+        let description = self
+            .description
+            .as_ref()
+            .map(|desc| quote! { .description(#desc) });
+
+        let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect();
+
+        let all_variants_unique = if var_types.len() > 1 {
+            Some(quote! { #crate_path::sa::assert_type_ne_all!(#(#var_types),*); })
+        } else {
+            None
+        };
+
+        let match_names = self.variants.iter().map(|var| {
+            let var_ty = &var.ty;
+            let var_check = &var.resolver_check;
+            quote! {
+                if #var_check {
+                    return <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())
+                        .unwrap().to_string();
+                }
+            }
+        });
+
+        let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect();
+        let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| {
+            let var_ty = &var.ty;
+
+            let get_name = quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) };
+            quote! {
+                if type_name == #get_name.unwrap() {
+                    return #crate_path::IntoResolvable::into(
+                        { #expr },
+                        executor.context()
+                    )
+                    .and_then(|res| match res {
+                        Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
+                        None => Ok(#crate_path::Value::null()),
+                    });
+                }
+            }
+        });
+        let resolve_into_type_async =
+            self.variants
+                .iter()
+                .zip(match_resolves.iter())
+                .map(|(var, expr)| {
+                    let var_ty = &var.ty;
+
+                    let get_name = quote! {
+                        (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()))
+                    };
+                    quote! {
+                        if type_name == #get_name.unwrap() {
+                            let res = #crate_path::IntoResolvable::into(
+                                { #expr },
+                                executor.context()
+                            );
+                            return #crate_path::futures::future::FutureExt::boxed(async move {
+                                match res? {
+                                    Some((ctx, r)) => {
+                                        let subexec = executor.replaced_context(ctx);
+                                        subexec.resolve_with_ctx_async(&(), &r).await
+                                    },
+                                    None => Ok(#crate_path::Value::null()),
+                                }
+                            });
+                        }
+                    }
+                });
+
+        let (_, ty_generics, _) = self.generics.split_for_impl();
+
+        let mut base_generics = self.generics.clone();
+        if self.is_trait_object {
+            base_generics.params.push(parse_quote! { '__obj });
+        }
+        let (impl_generics, _, _) = base_generics.split_for_impl();
+
+        let mut ext_generics = base_generics.clone();
+        if self.scalar.is_none() {
+            ext_generics.params.push(parse_quote! { #scalar });
+            ext_generics
+                .where_clause
+                .get_or_insert_with(|| parse_quote! { where })
+                .predicates
+                .push(parse_quote! { #scalar: #crate_path::ScalarValue });
+        }
+        let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl();
+
+        let mut where_async = where_clause
+            .cloned()
+            .unwrap_or_else(|| parse_quote! { where });
+        where_async
+            .predicates
+            .push(parse_quote! { Self: Send + Sync });
+        if self.scalar.is_none() {
+            where_async
+                .predicates
+                .push(parse_quote! { #scalar: Send + Sync });
+        }
+
+        let mut ty_full = quote! { #ty#ty_generics };
+        if self.is_trait_object {
+            ty_full = quote! { dyn #ty_full + '__obj + Send + Sync };
+        }
+
+        let type_impl = quote! {
+            #[automatically_derived]
+            impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty_full
+                #where_clause
+            {
+                type Context = #context;
+                type TypeInfo = ();
+
+                fn name(_ : &Self::TypeInfo) -> Option<&str> {
+                    Some(#name)
+                }
+
+                fn meta<'r>(
+                    info: &Self::TypeInfo,
+                    registry: &mut #crate_path::Registry<'r, #scalar>
+                ) -> #crate_path::meta::MetaType<'r, #scalar>
+                where #scalar: 'r,
+                {
+                    let types = &[
+                        #( registry.get_type::<&#var_types>(&(())), )*
+                    ];
+                    registry.build_union_type::<#ty_full>(info, types)
+                    #description
+                    .into_meta()
+                }
+
+                fn concrete_type_name(
+                    &self,
+                    context: &Self::Context,
+                    _: &Self::TypeInfo,
+                ) -> String {
+                    #( #match_names )*
+                    panic!(
+                        "GraphQL union {} cannot be resolved into any of its variants in its \
+                         current state",
+                        #name,
+                    );
+                }
+
+                fn resolve_into_type(
+                    &self,
+                    _: &Self::TypeInfo,
+                    type_name: &str,
+                    _: Option<&[#crate_path::Selection<#scalar>]>,
+                    executor: &#crate_path::Executor<Self::Context, #scalar>,
+                ) -> #crate_path::ExecutionResult<#scalar> {
+                    let context = executor.context();
+                    #( #resolve_into_type )*
+                    panic!(
+                        "Concrete type {} is not handled by instance resolvers on GraphQL union {}",
+                        type_name, #name,
+                    );
+                }
+            }
+        };
+
+        let async_type_impl = quote! {
+            #[automatically_derived]
+            impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty_full
+                #where_async
+            {
+                fn resolve_into_type_async<'b>(
+                    &'b self,
+                    _: &'b Self::TypeInfo,
+                    type_name: &str,
+                    _: Option<&'b [#crate_path::Selection<'b, #scalar>]>,
+                    executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar>
+                ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> {
+                    let context = executor.context();
+                    #( #resolve_into_type_async )*
+                    panic!(
+                        "Concrete type {} is not handled by instance resolvers on GraphQL union {}",
+                        type_name, #name,
+                    );
+                }
+            }
+        };
+
+        let output_type_impl = quote! {
+            #[automatically_derived]
+            impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty_full
+                #where_clause
+            {
+                fn mark() {
+                    #( <#var_types as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); )*
+                }
+            }
+        };
+
+        let union_impl = quote! {
+            #[automatically_derived]
+            impl#impl_generics #crate_path::marker::GraphQLUnion for #ty_full {
+                fn mark() {
+                    #all_variants_unique
+
+                    #( <#var_types as #crate_path::marker::GraphQLObjectType<
+                        #default_scalar,
+                    >>::mark(); )*
+                }
+            }
+        };
+
+        into.append_all(&[union_impl, output_type_impl, type_impl, async_type_impl]);
+    }
+}
+
+/// Emerges [`UnionMeta::external_resolvers`] into the given [GraphQL union][1] `variants`.
+///
+/// If duplication happens, then resolving code is overwritten with the one from
+/// `external_resolvers`.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+fn emerge_union_variants_from_meta(
+    variants: &mut Vec<UnionVariantDefinition>,
+    external_resolvers: UnionMetaResolvers,
+    mode: Mode,
+) {
+    if external_resolvers.is_empty() {
+        return;
+    }
+
+    let crate_path = mode.crate_path();
+
+    for (ty, rslvr) in external_resolvers {
+        let span = rslvr.span_joined();
+
+        let resolver_fn = rslvr.into_inner();
+        let resolver_code = parse_quote! {
+            #resolver_fn(self, #crate_path::FromContext::from(context))
+        };
+        // Doing this may be quite an expensive, because resolving may contain some heavy
+        // computation, so we're preforming it twice. Unfortunately, we have no other options here,
+        // until the `juniper::GraphQLType` itself will allow to do it in some cleverer way.
+        let resolver_check = parse_quote! {
+            ({ #resolver_code } as ::std::option::Option<&#ty>).is_some()
+        };
+
+        if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) {
+            var.resolver_code = resolver_code;
+            var.resolver_check = resolver_check;
+            var.span = span;
+        } else {
+            variants.push(UnionVariantDefinition {
+                ty,
+                resolver_code,
+                resolver_check,
+                enum_path: None,
+                context_ty: None,
+                span,
+            })
+        }
+    }
+}
+
+/// Checks whether all [GraphQL union][1] `variants` represent a different Rust type.
+///
+/// # Notice
+///
+/// This is not an optimal implementation, as it's possible to bypass this check by using a full
+/// qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, the
+/// static assertion [`assert_type_ne_all!`][2] is used to enforce this requirement in the generated
+/// code. However, due to the bad error message this implementation should stay and provide
+/// guidance.
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+/// [2]: https://docs.rs/static_assertions/latest/static_assertions/macro.assert_type_ne_all.html
+fn all_variants_different(variants: &Vec<UnionVariantDefinition>) -> bool {
+    let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect();
+    types.dedup();
+    types.len() == variants.len()
+}
diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs
index 4cee482f0..367792671 100644
--- a/juniper_codegen/src/impl_object.rs
+++ b/juniper_codegen/src/impl_object.rs
@@ -65,7 +65,7 @@ fn create(
             let _type = match method.sig.output {
                 syn::ReturnType::Type(_, ref t) => *t.clone(),
                 syn::ReturnType::Default => {
-                    error.custom(method.sig.span(), "return value required");
+                    error.emit_custom(method.sig.span(), "return value required");
                     return None;
                 }
             };
@@ -228,6 +228,7 @@ fn create(
         include_type_generics: false,
         generic_scalar: false,
         no_async: _impl.attrs.no_async.is_some(),
+        mode: is_internal.into(),
     };
 
     Ok(definition)
diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs
index 37a976583..fc449aa8a 100644
--- a/juniper_codegen/src/impl_scalar.rs
+++ b/juniper_codegen/src/impl_scalar.rs
@@ -264,7 +264,7 @@ pub fn build_scalar(
                 executor: &'a #crate_name::Executor<Self::Context, #async_generic_type>,
             ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<#async_generic_type>> {
                 use #crate_name::GraphQLType;
-                use futures::future;
+                use #crate_name::futures::future;
                 let v = self.resolve(info, selection_set, executor);
                 Box::pin(future::ready(v))
             }
diff --git a/juniper_codegen/src/impl_union.rs b/juniper_codegen/src/impl_union.rs
deleted file mode 100644
index fa9aea4b0..000000000
--- a/juniper_codegen/src/impl_union.rs
+++ /dev/null
@@ -1,223 +0,0 @@
-use crate::{
-    result::GraphQLScope,
-    util::{self, span_container::SpanContainer},
-};
-use proc_macro2::TokenStream;
-use quote::quote;
-use syn::{ext::IdentExt, spanned::Spanned};
-
-struct ResolverVariant {
-    pub ty: syn::Type,
-    pub resolver: syn::Expr,
-}
-
-struct ResolveBody {
-    pub variants: Vec<ResolverVariant>,
-}
-
-impl syn::parse::Parse for ResolveBody {
-    fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::parse::Error> {
-        input.parse::<syn::token::Match>()?;
-        input.parse::<syn::token::SelfValue>()?;
-
-        let match_body;
-        syn::braced!( match_body in input );
-
-        let mut variants = Vec::new();
-        while !match_body.is_empty() {
-            let ty = match_body.parse::<syn::Type>()?;
-            match_body.parse::<syn::token::FatArrow>()?;
-            let resolver = match_body.parse::<syn::Expr>()?;
-
-            variants.push(ResolverVariant { ty, resolver });
-
-            // Optinal trailing comma.
-            match_body.parse::<syn::token::Comma>().ok();
-        }
-
-        if !input.is_empty() {
-            return Err(input.error("unexpected input"));
-        }
-
-        Ok(Self { variants })
-    }
-}
-
-pub fn impl_union(
-    is_internal: bool,
-    attrs: TokenStream,
-    body: TokenStream,
-    error: GraphQLScope,
-) -> syn::Result<TokenStream> {
-    let body_span = body.span();
-    let _impl = util::parse_impl::ImplBlock::parse(attrs, body)?;
-
-    // FIXME: what is the purpose of this construct?
-    // Validate trait target name, if present.
-    if let Some((name, path)) = &_impl.target_trait {
-        if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") {
-            return Err(error.custom_error(
-                path.span(),
-                "Invalid impl target trait: expected 'GraphQLUnion'",
-            ));
-        }
-    }
-
-    let type_ident = &_impl.type_ident;
-    let name = _impl
-        .attrs
-        .name
-        .clone()
-        .map(SpanContainer::into_inner)
-        .unwrap_or_else(|| type_ident.unraw().to_string());
-    let crate_name = util::juniper_path(is_internal);
-
-    let scalar = _impl
-        .attrs
-        .scalar
-        .as_ref()
-        .map(|s| quote!( #s ))
-        .unwrap_or_else(|| {
-            quote! { #crate_name::DefaultScalarValue }
-        });
-
-    let method = _impl
-        .methods
-        .iter()
-        .find(|&m| _impl.parse_resolve_method(&m).is_ok());
-
-    let method = match method {
-        Some(method) => method,
-        None => {
-            return Err(error.custom_error(
-                body_span,
-                "expected exactly one method with signature: fn resolve(&self) { ... }",
-            ))
-        }
-    };
-
-    let resolve_args = _impl.parse_resolve_method(method)?;
-
-    let stmts = &method.block.stmts;
-    let body_raw = quote!( #( #stmts )* );
-    let body = syn::parse::<ResolveBody>(body_raw.into())?;
-
-    if body.variants.is_empty() {
-        error.not_empty(method.span())
-    }
-
-    proc_macro_error::abort_if_dirty();
-
-    let meta_types = body.variants.iter().map(|var| {
-        let var_ty = &var.ty;
-
-        quote! {
-            registry.get_type::<&#var_ty>(&(())),
-        }
-    });
-
-    let concrete_type_resolver = body.variants.iter().map(|var| {
-        let var_ty = &var.ty;
-        let resolve = &var.resolver;
-
-        quote! {
-            if ({#resolve} as std::option::Option<&#var_ty>).is_some() {
-                return <#var_ty as #crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string();
-            }
-        }
-    });
-
-    let resolve_into_type = body.variants.iter().map(|var| {
-        let var_ty = &var.ty;
-        let resolve = &var.resolver;
-
-        quote! {
-            if type_name == (<#var_ty as #crate_name::GraphQLType<#scalar>>::name(&())).unwrap() {
-                return executor.resolve(&(), &{ #resolve });
-            }
-        }
-    });
-
-    let generics = _impl.generics;
-    let (impl_generics, _, where_clause) = generics.split_for_impl();
-
-    let description = match _impl.description.as_ref() {
-        Some(value) => quote!( .description( #value ) ),
-        None => quote!(),
-    };
-    let context = _impl
-        .attrs
-        .context
-        .map(|c| quote! { #c })
-        .unwrap_or_else(|| quote! { () });
-
-    let ty = _impl.target_type;
-
-    let object_marks = body.variants.iter().map(|field| {
-        let _ty = &field.ty;
-        quote!(
-            <#_ty as #crate_name::marker::GraphQLObjectType<#scalar>>::mark();
-        )
-    });
-
-    let output = quote! {
-        impl #impl_generics #crate_name::marker::IsOutputType<#scalar> for #ty #where_clause {
-            fn mark() {
-                #( #object_marks )*
-            }
-        }
-
-        impl #impl_generics #crate_name::GraphQLType<#scalar> for #ty #where_clause
-        {
-            type Context = #context;
-            type TypeInfo = ();
-
-            fn name(_ : &Self::TypeInfo) -> Option<&str> {
-                Some(#name)
-            }
-
-            fn meta<'r>(
-                info: &Self::TypeInfo,
-                registry: &mut #crate_name::Registry<'r, #scalar>
-            ) -> #crate_name::meta::MetaType<'r, #scalar>
-                where
-                    #scalar: 'r,
-            {
-                let types = &[
-                    #( #meta_types )*
-                ];
-                registry.build_union_type::<#ty>(
-                    info, types
-                )
-                    #description
-                    .into_meta()
-            }
-
-            #[allow(unused_variables)]
-            fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String {
-                #( #concrete_type_resolver )*
-
-                panic!("Concrete type not handled by instance resolvers on {}", #name);
-            }
-
-            fn resolve_into_type(
-                &self,
-                _info: &Self::TypeInfo,
-                type_name: &str,
-                _: Option<&[#crate_name::Selection<#scalar>]>,
-                executor: &#crate_name::Executor<Self::Context, #scalar>,
-            ) -> #crate_name::ExecutionResult<#scalar> {
-                let context = &executor.context();
-                #( #resolve_args )*
-
-                #( #resolve_into_type )*
-
-                 panic!("Concrete type not handled by instance resolvers on {}", #name);
-            }
-        }
-
-
-    };
-
-    Ok(output.into())
-}
diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs
index 55ffc1080..9e6578910 100644
--- a/juniper_codegen/src/lib.rs
+++ b/juniper_codegen/src/lib.rs
@@ -16,15 +16,17 @@ mod derive_enum;
 mod derive_input_object;
 mod derive_object;
 mod derive_scalar_value;
-mod derive_union;
 mod impl_object;
 mod impl_scalar;
-mod impl_union;
+
+mod graphql_union;
 
 use proc_macro::TokenStream;
-use proc_macro_error::proc_macro_error;
+use proc_macro_error::{proc_macro_error, ResultExt as _};
 use result::GraphQLScope;
 
+use self::util::Mode;
+
 #[proc_macro_error]
 #[proc_macro_derive(GraphQLEnum, attributes(graphql))]
 pub fn derive_enum(input: TokenStream) -> TokenStream {
@@ -93,16 +95,6 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream {
     }
 }
 
-#[proc_macro_error]
-#[proc_macro_derive(GraphQLUnion, attributes(graphql))]
-pub fn derive_union(input: TokenStream) -> TokenStream {
-    let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
-    let gen = derive_union::build_derive_union(ast, false, GraphQLScope::DeriveUnion);
-    match gen {
-        Ok(gen) => gen.into(),
-        Err(err) => proc_macro_error::abort!(err),
-    }
-}
 /// This custom derive macro implements the #[derive(GraphQLScalarValue)]
 /// derive.
 ///
@@ -554,27 +546,616 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T
     ))
 }
 
+/// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and
+/// structs.
+///
+/// The `#[graphql]` helper attribute is used for configuring the derived implementation. Specifying
+/// multiple `#[graphql]` attributes on the same definition is totally okay. They all will be
+/// treated as a single attribute.
+///
+/// ```
+/// 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 CharacterEnum {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+/// ```
+///
+/// # Custom name and description
+///
+/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default,
+/// a type name is used.
+///
+/// The description of [GraphQL union][1] may be specified either with a `description`/`desc`
+/// attribute's argument, or with a regular Rust doc comment.
+///
+/// ```
+/// # use juniper::{GraphQLObject, GraphQLUnion};
+/// #
+/// # #[derive(GraphQLObject)]
+/// # struct Human {
+/// #    id: String,
+/// #    home_planet: String,
+/// # }
+/// #
+/// # #[derive(GraphQLObject)]
+/// # struct Droid {
+/// #     id: String,
+/// #     primary_function: String,
+/// # }
+/// #
+/// #[derive(GraphQLUnion)]
+/// #[graphql(name = "Character", desc = "Possible episode characters.")]
+/// enum Chrctr {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+///
+/// // NOTICE: Rust docs are used as GraphQL description.
+/// /// Possible episode characters.
+/// #[derive(GraphQLUnion)]
+/// enum CharacterWithDocs {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+///
+/// // NOTICE: `description` argument takes precedence over Rust docs.
+/// /// Not a GraphQL description anymore.
+/// #[derive(GraphQLUnion)]
+/// #[graphql(description = "Possible episode characters.")]
+/// enum CharacterWithDescription {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+/// ```
+///
+/// # Custom context
+///
+/// By default, the generated implementation uses [unit type `()`][4] as context. To use a custom
+/// context type for [GraphQL union][1] variants types or external resolver functions, specify it
+/// with `context`/`Context` attribute's argument.
+///
+/// ```
+/// # 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;
+/// impl juniper::Context for CustomContext {}
+///
+/// #[derive(GraphQLUnion)]
+/// #[graphql(Context = CustomContext)]
+/// enum Character {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+/// ```
+///
+/// # Custom `ScalarValue`
+///
+/// By default, this macro generates code, which is generic over a `ScalarValue` type.
+/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a
+/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete
+/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's
+/// argument.
+///
+/// ```
+/// # 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,
+/// }
+///
+/// // NOTICE: Removing `Scalar` argument will fail compilation.
+/// #[derive(GraphQLUnion)]
+/// #[graphql(Scalar = DefaultScalarValue)]
+/// enum Character {
+///     Human(Human),
+///     Droid(Droid),
+/// }
+/// ```
+///
+/// # Ignoring enum variants
+///
+/// To omit exposing an enum variant in the GraphQL schema, use an `ignore`/`skip` attribute's
+/// argument directly on that variant.
+///
+/// > __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__.
+///
+/// ```
+/// # 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>),
+/// }
+/// ```
+///
+/// # External resolver functions
+///
+/// To use a custom logic for resolving a [GraphQL union][1] variant, an external resolver function
+/// may be specified with:
+/// - either a `with` attribute's argument on an enum variant;
+/// - or an `on` attribute's argument on an enum/struct itself.
+///
+/// ```
+/// # 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)
+///     }
+/// }
+///
+/// #[derive(GraphQLUnion)]
+/// #[graphql(Context = CustomContext)]
+/// #[graphql(on Droid = CharacterWithoutDroid::droid_from_context)]
+/// enum CharacterWithoutDroid {
+///     Human(Human),
+///     #[graphql(ignore)]
+///     Droid,
+/// }
+///
+/// impl CharacterWithoutDroid {
+///     fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> {
+///         if let Self::Droid = self {
+///             Some(&ctx.droid)
+///         } else {
+///             None
+///         }
+///     }
+/// }
+/// ```
+///
+/// # Deriving structs
+///
+/// Specifying external resolver functions is mandatory for using a struct as a [GraphQL union][1],
+/// because this is the only way to declare [GraphQL union][1] variants in this case.
+///
+/// ```
+/// # 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)
+///     }
+/// }
+/// ```
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html
+#[proc_macro_error]
+#[proc_macro_derive(GraphQLUnion, attributes(graphql))]
+pub fn derive_union(input: TokenStream) -> TokenStream {
+    self::graphql_union::derive::expand(input.into(), Mode::Public)
+        .unwrap_or_abort()
+        .into()
+}
+
+#[proc_macro_error]
+#[proc_macro_derive(GraphQLUnionInternal, attributes(graphql))]
+#[doc(hidden)]
+pub fn derive_union_internal(input: TokenStream) -> TokenStream {
+    self::graphql_union::derive::expand(input.into(), Mode::Internal)
+        .unwrap_or_abort()
+        .into()
+}
+
+/// `#[graphql_union]` macro for deriving a [GraphQL union][1] implementation for traits.
+///
+/// Specifying multiple `#[graphql_union]` attributes on the same definition is totally okay. They
+/// all will be treated as a single attribute.
+///
+/// A __trait has to be [object safe][2]__, because schema resolvers will need to return a
+/// [trait object][3] to specify a [GraphQL union][1] behind it. The [trait object][3] has to be
+/// [`Send`] and [`Sync`].
+///
+/// ```
+/// 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) }
+/// }
+/// ```
+///
+/// # Custom name and description
+///
+/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default,
+/// a type name is used.
+///
+/// The description of [GraphQL union][1] may be specified either with a `description`/`desc`
+/// attribute's argument, or with a regular Rust doc comment.
+///
+/// ```
+/// # 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(name = "Character", desc = "Possible episode characters.")]
+/// trait Chrctr {
+///     fn as_human(&self) -> Option<&Human> { None }
+///     fn as_droid(&self) -> Option<&Droid> { None }
+/// }
+///
+/// // NOTICE: Rust docs are used as GraphQL description.
+/// /// Possible episode characters.
+/// trait CharacterWithDocs {
+///     fn as_human(&self) -> Option<&Human> { None }
+///     fn as_droid(&self) -> Option<&Droid> { None }
+/// }
+///
+/// // NOTICE: `description` argument takes precedence over Rust docs.
+/// /// Not a GraphQL description anymore.
+/// #[graphql_union(description = "Possible episode characters.")]
+/// trait CharacterWithDescription {
+///     fn as_human(&self) -> Option<&Human> { None }
+///     fn as_droid(&self) -> Option<&Droid> { None }
+/// }
+/// #
+/// # impl Chrctr for Human {}
+/// # impl Chrctr for Droid {}
+/// # impl CharacterWithDocs for Human {}
+/// # impl CharacterWithDocs for Droid {}
+/// # impl CharacterWithDescription for Human {}
+/// # impl CharacterWithDescription for Droid {}
+/// ```
+///
+/// # Custom context
+///
+/// By default, the generated implementation tries to infer `juniper::Context` type from signatures
+/// of trait methods, and uses [unit type `()`][4] if signatures contains no context arguments.
+///
+/// If `juniper::Context` type cannot be inferred or is inferred incorrectly, then specify it
+/// explicitly with `context`/`Context` attribute's argument.
+///
+/// ```
+/// # 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 {
+///     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)
+///     }
+/// }
+/// ```
+///
+/// # Custom `ScalarValue`
+///
+/// By default, `#[graphql_union]` macro generates code, which is generic over a `ScalarValue` type.
+/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a
+/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete
+/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's
+/// argument.
+///
+/// ```
+/// # use juniper::{graphql_union, DefaultScalarValue, GraphQLObject};
+/// #
+/// #[derive(GraphQLObject)]
+/// #[graphql(Scalar = DefaultScalarValue)]
+/// struct Human {
+///     id: String,
+///     home_planet: String,
+/// }
+///
+/// #[derive(GraphQLObject)]
+/// struct Droid {
+///     id: String,
+///     primary_function: String,
+/// }
+///
+/// // NOTICE: Removing `Scalar` argument will fail compilation.
+/// #[graphql_union(Scalar = DefaultScalarValue)]
+/// trait Character {
+///     fn as_human(&self) -> Option<&Human> { None }
+///     fn as_droid(&self) -> Option<&Droid> { None }
+/// }
+/// #
+/// # impl Character for Human {}
+/// # impl Character for Droid {}
+/// ```
+///
+/// # Ignoring trait methods
+///
+/// To omit some trait method to be assumed as a [GraphQL union][1] variant and ignore it, use an
+/// `ignore`/`skip` attribute's argument directly on that method.
+///
+/// ```
+/// # 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_union(ignore)]  // or `#[graphql_union(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() }
+/// # }
+/// ```
+///
+/// # External resolver functions
+///
+/// It's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead
+/// custom functions may be specified with an `on` attribute's argument.
+///
+/// ```
+/// # 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_union(ignore)]
+///     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() }
+/// }
+///
+/// // NOTICE: 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())
+///     }
+/// }
+///
+/// // NOTICE: Custom 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())
+/// }
+/// ```
+///
+/// [1]: https://spec.graphql.org/June2018/#sec-Unions
+/// [2]: https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety
+/// [3]: https://doc.rust-lang.org/stable/reference/types/trait-object.html
+/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html
 #[proc_macro_error]
 #[proc_macro_attribute]
-pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream {
-    let attrs = proc_macro2::TokenStream::from(attrs);
-    let body = proc_macro2::TokenStream::from(body);
-    let gen = impl_union::impl_union(false, attrs, body, GraphQLScope::ImplUnion);
-    match gen {
-        Ok(gen) => gen.into(),
-        Err(err) => proc_macro_error::abort!(err),
-    }
+pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream {
+    self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Public)
+        .unwrap_or_abort()
+        .into()
 }
 
 #[proc_macro_error]
 #[proc_macro_attribute]
 #[doc(hidden)]
-pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream {
-    let attrs = proc_macro2::TokenStream::from(attrs);
-    let body = proc_macro2::TokenStream::from(body);
-    let gen = impl_union::impl_union(true, attrs, body, GraphQLScope::ImplUnion);
-    match gen {
-        Ok(gen) => gen.into(),
-        Err(err) => proc_macro_error::abort!(err),
-    }
+pub fn graphql_union_internal(attr: TokenStream, body: TokenStream) -> TokenStream {
+    self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal)
+        .unwrap_or_abort()
+        .into()
 }
diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs
index dea2a5532..73a495961 100644
--- a/juniper_codegen/src/result.rs
+++ b/juniper_codegen/src/result.rs
@@ -5,28 +5,29 @@ use proc_macro2::Span;
 use proc_macro_error::{Diagnostic, Level};
 use std::fmt;
 
-pub const GRAPHQL_SPECIFICATION: &'static str = "https://spec.graphql.org/June2018/";
+/// URL of the GraphQL specification (June 2018 Edition).
+pub const SPEC_URL: &'static str = "https://spec.graphql.org/June2018/";
 
 #[allow(unused_variables)]
 pub enum GraphQLScope {
+    UnionAttr,
     DeriveObject,
     DeriveInputObject,
-    DeriveUnion,
+    UnionDerive,
     DeriveEnum,
     DeriveScalar,
-    ImplUnion,
     ImplScalar,
     ImplObject,
 }
 
 impl GraphQLScope {
-    pub fn specification_section(&self) -> &str {
+    pub fn spec_section(&self) -> &str {
         match self {
-            GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "#sec-Objects",
-            GraphQLScope::DeriveInputObject => "#sec-Input-Objects",
-            GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "#sec-Unions",
-            GraphQLScope::DeriveEnum => "#sec-Enums",
-            GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "#sec-Scalars",
+            Self::DeriveObject | Self::ImplObject => "#sec-Objects",
+            Self::DeriveInputObject => "#sec-Input-Objects",
+            Self::UnionAttr | Self::UnionDerive => "#sec-Unions",
+            Self::DeriveEnum => "#sec-Enums",
+            Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars",
         }
     }
 }
@@ -34,11 +35,11 @@ impl GraphQLScope {
 impl fmt::Display for GraphQLScope {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let name = match self {
-            GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "object",
-            GraphQLScope::DeriveInputObject => "input object",
-            GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "union",
-            GraphQLScope::DeriveEnum => "enum",
-            GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "scalar",
+            Self::DeriveObject | Self::ImplObject => "object",
+            Self::DeriveInputObject => "input object",
+            Self::UnionAttr | Self::UnionDerive => "union",
+            Self::DeriveEnum => "enum",
+            Self::DeriveScalar | Self::ImplScalar => "scalar",
         };
 
         write!(f, "GraphQL {}", name)
@@ -51,20 +52,22 @@ pub enum UnsupportedAttribute {
     Skip,
     Interface,
     Scalar,
-    Description,
     Deprecation,
     Default,
 }
 
 impl GraphQLScope {
-    fn specification_link(&self) -> String {
-        format!("{}{}", GRAPHQL_SPECIFICATION, self.specification_section())
+    fn spec_link(&self) -> String {
+        format!("{}{}", SPEC_URL, self.spec_section())
     }
 
-    pub fn custom<S: AsRef<str>>(&self, span: Span, msg: S) {
+    pub fn custom<S: AsRef<str>>(&self, span: Span, msg: S) -> Diagnostic {
         Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref()))
-            .note(self.specification_link())
-            .emit();
+            .note(self.spec_link())
+    }
+
+    pub fn emit_custom<S: AsRef<str>>(&self, span: Span, msg: S) {
+        self.custom(span, msg).emit()
     }
 
     pub fn custom_error<S: AsRef<str>>(&self, span: Span, msg: S) -> syn::Error {
@@ -97,7 +100,7 @@ impl GraphQLScope {
             Level::Error,
             format!("{} expects at least one field", self),
         )
-        .note(self.specification_link())
+        .note(self.spec_link())
         .emit();
     }
 
@@ -120,7 +123,7 @@ impl GraphQLScope {
                             ),
                         )
                             .help(format!("There is at least one other field with the same name `{}`, possibly renamed via the #[graphql] attribute", dup.name))
-                            .note(self.specification_link())
+                            .note(self.spec_link())
                             .emit();
                     });
             })
@@ -130,9 +133,12 @@ impl GraphQLScope {
         Diagnostic::spanned(
             field,
             Level::Error,
-            "All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system.".to_string(),
+            "All types and directives defined within a schema must not have a name which begins \
+             with `__` (two underscores), as this is used exclusively by GraphQL’s introspection \
+             system."
+                .into(),
         )
-            .note(format!("{}#sec-Schema", GRAPHQL_SPECIFICATION))
-            .emit();
+        .note(format!("{}#sec-Schema", SPEC_URL))
+        .emit();
     }
 }
diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs
index 60ca23dee..82351e616 100644
--- a/juniper_codegen/src/util/mod.rs
+++ b/juniper_codegen/src/util/mod.rs
@@ -1,9 +1,13 @@
 #![allow(clippy::single_match)]
 
 pub mod duplicate;
+pub mod mode;
+pub mod option_ext;
 pub mod parse_impl;
 pub mod span_container;
 
+use std::ops::Deref as _;
+
 use proc_macro2::{Span, TokenStream};
 use proc_macro_error::abort;
 use quote::quote;
@@ -14,10 +18,7 @@ use syn::{
     MetaNameValue, NestedMeta, Token,
 };
 
-pub fn juniper_path(is_internal: bool) -> syn::Path {
-    let name = if is_internal { "crate" } else { "juniper" };
-    syn::parse_str::<syn::Path>(name).unwrap()
-}
+pub use self::{mode::Mode, option_ext::OptionExt};
 
 /// Returns the name of a type.
 /// If the type does not end in a simple ident, `None` is returned.
@@ -74,6 +75,15 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool {
     }
 }
 
+/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested
+/// [`syn::TypeParen`]s asap).
+pub fn unparenthesize(ty: &syn::Type) -> &syn::Type {
+    match ty {
+        syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()),
+        _ => ty,
+    }
+}
+
 #[derive(Debug)]
 pub struct DeprecationAttr {
     pub reason: Option<String>,
@@ -85,6 +95,16 @@ pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> {
         .find(|attr| path_eq_single(&attr.path, "graphql"))
 }
 
+/// Filters given `attrs` to contain attributes only with the given `name`.
+pub fn filter_attrs<'a>(
+    name: &'a str,
+    attrs: &'a [Attribute],
+) -> impl Iterator<Item = &'a Attribute> + 'a {
+    attrs
+        .iter()
+        .filter(move |attr| path_eq_single(&attr.path, name))
+}
+
 pub fn get_deprecated(attrs: &[Attribute]) -> Option<SpanContainer<DeprecationAttr>> {
     attrs
         .iter()
@@ -669,6 +689,7 @@ pub struct GraphQLTypeDefiniton {
     pub generic_scalar: bool,
     // FIXME: make this redundant.
     pub no_async: bool,
+    pub mode: Mode,
 }
 
 impl GraphQLTypeDefiniton {
@@ -867,7 +888,7 @@ impl GraphQLTypeDefiniton {
                                     Err(e) => Err(e),
                                 }
                             };
-                            use futures::future;
+                            use #juniper_crate_name::futures::future;
                             future::FutureExt::boxed(f)
                         },
                     )
@@ -884,7 +905,7 @@ impl GraphQLTypeDefiniton {
                                     Err(e) => Err(e),
                                 }
                             };
-                            use futures::future;
+                            use #juniper_crate_name::futures::future;
                             future::FutureExt::boxed(f)
                         )
                     } else {
@@ -894,7 +915,7 @@ impl GraphQLTypeDefiniton {
                                 Ok(None) => Ok(#juniper_crate_name::Value::null()),
                                 Err(e) => Err(e),
                             };
-                            use futures::future;
+                            use #juniper_crate_name::futures::future;
                             future::FutureExt::boxed(future::ready(v))
                         )
                     };
@@ -934,7 +955,7 @@ impl GraphQLTypeDefiniton {
                     ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>>
                         where #scalar: Send + Sync,
                     {
-                        use futures::future;
+                        use #juniper_crate_name::futures::future;
                         use #juniper_crate_name::GraphQLType;
                         match field {
                             #( #resolve_matches_async )*
@@ -1177,7 +1198,7 @@ impl GraphQLTypeDefiniton {
                 };
                 quote!(
                     #name => {
-                        futures::FutureExt::boxed(async move {
+                        #juniper_crate_name::futures::FutureExt::boxed(async move {
                             let res #_type = { #code };
                             let res = #juniper_crate_name::IntoFieldResult::<_, #scalar>::into_result(res)?;
                             let executor= executor.as_owned_executor();
@@ -1267,7 +1288,7 @@ impl GraphQLTypeDefiniton {
                     args: #juniper_crate_name::Arguments<'args, #scalar>,
                     executor: &'ref_e #juniper_crate_name::Executor<'ref_e, 'e, Self::Context, #scalar>,
                 ) -> std::pin::Pin<Box<
-                        dyn futures::future::Future<
+                        dyn #juniper_crate_name::futures::future::Future<
                             Output = Result<
                                 #juniper_crate_name::Value<#juniper_crate_name::ValuesStream<'res, #scalar>>,
                                 #juniper_crate_name::FieldError<#scalar>
@@ -1284,7 +1305,7 @@ impl GraphQLTypeDefiniton {
                         'res: 'f,
                 {
                     use #juniper_crate_name::Value;
-                    use futures::stream::StreamExt as _;
+                    use #juniper_crate_name::futures::stream::StreamExt as _;
 
                     match field_name {
                             #( #resolve_matches_async )*
@@ -1302,245 +1323,6 @@ impl GraphQLTypeDefiniton {
         )
     }
 
-    pub fn into_union_tokens(self, juniper_crate_name: &str) -> TokenStream {
-        let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
-
-        let name = &self.name;
-        let ty = &self._type;
-        let context = self
-            .context
-            .as_ref()
-            .map(|ctx| quote!( #ctx ))
-            .unwrap_or_else(|| quote!(()));
-
-        let scalar = self
-            .scalar
-            .as_ref()
-            .map(|s| quote!( #s ))
-            .unwrap_or_else(|| {
-                if self.generic_scalar {
-                    // If generic_scalar is true, we always insert a generic scalar.
-                    // See more comments below.
-                    quote!(__S)
-                } else {
-                    quote!(#juniper_crate_name::DefaultScalarValue)
-                }
-            });
-
-        let description = self
-            .description
-            .as_ref()
-            .map(|description| quote!( .description(#description) ));
-
-        let meta_types = self.fields.iter().map(|field| {
-            let var_ty = &field._type;
-
-            quote! {
-                registry.get_type::<&#var_ty>(&(())),
-            }
-        });
-
-        let matcher_variants = self
-            .fields
-            .iter()
-            .map(|field| {
-                let var_ty = &field._type;
-                let resolver_code = &field.resolver_code;
-
-                quote!(
-                    #resolver_code(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(),
-                )
-            });
-
-        let concrete_type_resolver = quote!(
-            match self {
-                #( #matcher_variants )*
-            }
-        );
-
-        let matcher_expr: Vec<_> = self
-            .fields
-            .iter()
-            .map(|field| {
-                let resolver_code = &field.resolver_code;
-
-                quote!(
-                    match self { #resolver_code(ref val) => Some(val), _ => None, }
-                )
-            })
-            .collect();
-
-        let resolve_into_type = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| {
-            let var_ty = &field._type;
-
-            quote! {
-                if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() {
-                    return #juniper_crate_name::IntoResolvable::into(
-                        { #expr },
-                        executor.context()
-                    )
-                    .and_then(|res| {
-                        match res {
-                            Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
-                            None => Ok(#juniper_crate_name::Value::null()),
-                        }
-                    });
-                }
-            }
-        });
-
-        let resolve_into_type_async = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| {
-            let var_ty = &field._type;
-
-            quote! {
-                if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() {
-                    let inner_res = #juniper_crate_name::IntoResolvable::into(
-                        { #expr },
-                        executor.context()
-                    );
-
-                    let f = async move {
-                        match inner_res {
-                            Ok(Some((ctx, r))) => {
-                                let subexec = executor.replaced_context(ctx);
-                                subexec.resolve_with_ctx_async(&(), &r).await
-                            },
-                            Ok(None) => Ok(#juniper_crate_name::Value::null()),
-                            Err(e) => Err(e),
-                        }
-                    };
-                    use futures::future;
-                    return future::FutureExt::boxed(f);
-                }
-            }
-        });
-
-        let mut generics = self.generics.clone();
-
-        if self.scalar.is_none() && self.generic_scalar {
-            // No custom scalar specified, but always generic specified.
-            // Therefore we inject the generic scalar.
-
-            generics.params.push(parse_quote!(__S));
-
-            let where_clause = generics.where_clause.get_or_insert(parse_quote!(where));
-            // Insert ScalarValue constraint.
-            where_clause
-                .predicates
-                .push(parse_quote!(__S: #juniper_crate_name::ScalarValue));
-        }
-
-        let (impl_generics, _, where_clause) = generics.split_for_impl();
-
-        let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where));
-        where_async
-            .predicates
-            .push(parse_quote!( #scalar: Send + Sync ));
-        where_async.predicates.push(parse_quote!(Self: Send + Sync));
-
-        let async_type_impl = quote!(
-            impl#impl_generics #juniper_crate_name::GraphQLTypeAsync<#scalar> for #ty
-                #where_async
-            {
-                fn resolve_into_type_async<'b>(
-                    &'b self,
-                    _info: &'b Self::TypeInfo,
-                    type_name: &str,
-                    _: Option<&'b [#juniper_crate_name::Selection<'b, #scalar>]>,
-                    executor: &'b #juniper_crate_name::Executor<'b, 'b, Self::Context, #scalar>
-                ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> {
-                    let context = &executor.context();
-
-                    #( #resolve_into_type_async )*
-
-                    panic!("Concrete type not handled by instance resolvers on {}", #name);
-                }
-            }
-        );
-
-        let convesion_impls = self.fields.iter().map(|field| {
-            let variant_ty = &field._type;
-            let resolver_code = &field.resolver_code;
-
-            quote!(
-                impl std::convert::From<#variant_ty> for #ty {
-                    fn from(val: #variant_ty) -> Self {
-                        #resolver_code(val)
-                    }
-                }
-            )
-        });
-
-        let object_marks = self.fields.iter().map(|field| {
-            let _ty = &field._type;
-            quote!(
-                <#_ty as #juniper_crate_name::marker::GraphQLObjectType<#scalar>>::mark();
-            )
-        });
-
-        let mut type_impl = quote! {
-            #( #convesion_impls )*
-
-            impl #impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty #where_clause {
-                fn mark() {
-                    #( #object_marks )*
-                }
-            }
-
-            impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause
-            {
-                type Context = #context;
-                type TypeInfo = ();
-
-                fn name(_ : &Self::TypeInfo) -> Option<&str> {
-                    Some(#name)
-                }
-
-                fn meta<'r>(
-                    info: &Self::TypeInfo,
-                    registry: &mut #juniper_crate_name::Registry<'r, #scalar>
-                ) -> #juniper_crate_name::meta::MetaType<'r, #scalar>
-                    where
-                        #scalar: 'r,
-                {
-                    let types = &[
-                        #( #meta_types )*
-                    ];
-                    registry.build_union_type::<#ty>(
-                        info, types
-                    )
-                    #description
-                    .into_meta()
-                }
-
-                #[allow(unused_variables)]
-                fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String {
-                    #concrete_type_resolver
-                }
-
-                fn resolve_into_type(
-                    &self,
-                    _info: &Self::TypeInfo,
-                    type_name: &str,
-                    _: Option<&[#juniper_crate_name::Selection<#scalar>]>,
-                    executor: &#juniper_crate_name::Executor<Self::Context, #scalar>,
-                ) -> #juniper_crate_name::ExecutionResult<#scalar> {
-                    let context = &executor.context();
-
-                    #( #resolve_into_type )*
-
-                    panic!("Concrete type not handled by instance resolvers on {}", #name);
-                }
-            }
-        };
-
-        if !self.no_async {
-            type_impl.extend(async_type_impl)
-        }
-
-        type_impl
-    }
-
     pub fn into_enum_tokens(self, juniper_crate_name: &str) -> TokenStream {
         let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
 
@@ -1660,7 +1442,7 @@ impl GraphQLTypeDefiniton {
                     executor: &'a #juniper_crate_name::Executor<Self::Context, #scalar>,
                 ) -> #juniper_crate_name::BoxFuture<'a, #juniper_crate_name::ExecutionResult<#scalar>> {
                     use #juniper_crate_name::GraphQLType;
-                    use futures::future;
+                    use #juniper_crate_name::futures::future;
                     let v = self.resolve(info, selection_set, executor);
                     future::FutureExt::boxed(future::ready(v))
                 }
diff --git a/juniper_codegen/src/util/mode.rs b/juniper_codegen/src/util/mode.rs
new file mode 100644
index 000000000..c236309bf
--- /dev/null
+++ b/juniper_codegen/src/util/mode.rs
@@ -0,0 +1,32 @@
+//! Code generation mode.
+
+/// Code generation mode for macros.
+#[derive(Clone, Copy, Debug)]
+pub enum Mode {
+    /// Generated code is intended to be used by library users.
+    Public,
+
+    /// Generated code is use only inside the library itself.
+    Internal,
+}
+
+impl Mode {
+    pub fn crate_path(&self) -> syn::Path {
+        syn::parse_str::<syn::Path>(match self {
+            Self::Public => "::juniper",
+            Self::Internal => "crate",
+        })
+        .unwrap_or_else(|e| proc_macro_error::abort!(e))
+    }
+}
+
+// TODO: Remove once all macros are refactored with `Mode`.
+impl From<bool> for Mode {
+    fn from(is_internal: bool) -> Self {
+        if is_internal {
+            Mode::Internal
+        } else {
+            Mode::Public
+        }
+    }
+}
diff --git a/juniper_codegen/src/util/option_ext.rs b/juniper_codegen/src/util/option_ext.rs
new file mode 100644
index 000000000..3007abd65
--- /dev/null
+++ b/juniper_codegen/src/util/option_ext.rs
@@ -0,0 +1,24 @@
+/// Handy extension of [`Option`] methods used in this crate.
+pub trait OptionExt {
+    type Inner;
+
+    /// Transforms the `Option<T>` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)`
+    /// to `Err(err(v))`.
+    fn none_or_else<E, F>(self, err: F) -> Result<(), E>
+    where
+        F: FnOnce(Self::Inner) -> E;
+}
+
+impl<T> OptionExt for Option<T> {
+    type Inner = T;
+
+    fn none_or_else<E, F>(self, err: F) -> Result<(), E>
+    where
+        F: FnOnce(T) -> E,
+    {
+        match self {
+            Some(v) => Err(err(v)),
+            None => Ok(()),
+        }
+    }
+}
diff --git a/juniper_codegen/src/util/parse_impl.rs b/juniper_codegen/src/util/parse_impl.rs
index 93bc26bd6..0aa8392dd 100644
--- a/juniper_codegen/src/util/parse_impl.rs
+++ b/juniper_codegen/src/util/parse_impl.rs
@@ -19,36 +19,6 @@ pub struct ImplBlock {
 }
 
 impl ImplBlock {
-    /// Parse a `fn resolve()` method declaration found in most
-    /// generators which rely on `impl` blocks.
-    pub fn parse_resolve_method(
-        &self,
-        method: &syn::ImplItemMethod,
-    ) -> syn::Result<Vec<TokenStream>> {
-        if method.sig.ident != "resolve" {
-            return Err(syn::Error::new(
-                method.sig.ident.span(),
-                "expect the method named `resolve`",
-            ));
-        }
-
-        if let syn::ReturnType::Type(_, _) = &method.sig.output {
-            return Err(syn::Error::new(
-                method.sig.output.span(),
-                "method must not have a declared return type",
-            ));
-        }
-
-        //NOTICE: `fn resolve()` is a subset of `fn <NAME>() -> <TYPE>`
-        self.parse_method(method, false, |captured, _, _| {
-            Err(syn::Error::new(
-                captured.span(),
-                "only executor or context types are allowed",
-            ))
-        })
-        .map(|(tokens, _empty)| tokens)
-    }
-
     /// Parse a `fn <NAME>() -> <TYPE>` method declaration found in
     /// objects.
     pub fn parse_method<
diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs
index a808bbeba..f335da9fe 100644
--- a/juniper_codegen/src/util/span_container.rs
+++ b/juniper_codegen/src/util/span_container.rs
@@ -1,8 +1,12 @@
+use std::{
+    hash::{Hash, Hasher},
+    ops,
+};
+
 use proc_macro2::{Span, TokenStream};
 use quote::ToTokens;
-use std::cmp::{Eq, PartialEq};
 
-#[derive(Debug, Clone)]
+#[derive(Clone, Debug)]
 pub struct SpanContainer<T> {
     expr: Option<Span>,
     ident: Span,
@@ -24,6 +28,19 @@ impl<T> SpanContainer<T> {
         self.ident
     }
 
+    pub fn span_joined(&self) -> Span {
+        if let Some(s) = self.expr {
+            // TODO: Use `Span::join` once stabilized and available on stable:
+            //       https://github.com/rust-lang/rust/issues/54725
+            // self.ident.join(s).unwrap()
+
+            // At the moment, just return the second, more meaningful part.
+            s
+        } else {
+            self.ident
+        }
+    }
+
     pub fn into_inner(self) -> T {
         self.val
     }
@@ -47,7 +64,7 @@ impl<T> AsRef<T> for SpanContainer<T> {
     }
 }
 
-impl<T> std::ops::Deref for SpanContainer<T> {
+impl<T> ops::Deref for SpanContainer<T> {
     type Target = T;
 
     fn deref(&self) -> &Self::Target {
@@ -68,3 +85,12 @@ impl<T: PartialEq> PartialEq<T> for SpanContainer<T> {
         &self.val == other
     }
 }
+
+impl<T: Hash> Hash for SpanContainer<T> {
+    fn hash<H>(&self, state: &mut H)
+    where
+        H: Hasher,
+    {
+        self.val.hash(state)
+    }
+}


Test patch:
diff --git a/docs/book/tests/Cargo.toml b/docs/book/tests/Cargo.toml
index d7c4d0bc9..120f1a894 100644
--- a/docs/book/tests/Cargo.toml
+++ b/docs/book/tests/Cargo.toml
@@ -9,6 +9,8 @@ build = "build.rs"
 juniper = { path = "../../../juniper" }
 juniper_iron = { path = "../../../juniper_iron" }
 juniper_subscriptions = { path = "../../../juniper_subscriptions" }
+
+derive_more = "0.99.7"
 futures = "0.3"
 tokio = { version = "0.2", features = ["rt-core", "blocking", "stream", "rt-util"] }
 iron = "0.5.0"
diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs
new file mode 100644
index 000000000..76d7b27ea
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs
@@ -0,0 +1,6 @@
+use juniper::graphql_union;
+
+#[graphql_union]
+enum Character {}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr
new file mode 100644
index 000000000..419835d51
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr
@@ -0,0 +1,7 @@
+error: #[graphql_union] attribute is applicable to trait definitions only
+ --> $DIR/attr_wrong_item.rs:3:1
+  |
+3 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^
+  |
+  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.rs b/integration_tests/codegen_fail/fail/union/derive_enum_field.rs
deleted file mode 100644
index 11b945065..000000000
--- a/integration_tests/codegen_fail/fail/union/derive_enum_field.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-#[derive(juniper::GraphQLEnum)]
-pub enum Test {
-    A,
-    B,
-}
-
-#[derive(juniper::GraphQLUnion)]
-enum Character {
-    Test(Test),
-}
-
-fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr b/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr
deleted file mode 100644
index 58d13adf2..000000000
--- a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr
+++ /dev/null
@@ -1,8 +0,0 @@
-error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied
- --> $DIR/derive_enum_field.rs:7:10
-  |
-7 | #[derive(juniper::GraphQLUnion)]
-  |          ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test`
-  |
-  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
-  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs b/integration_tests/codegen_fail/fail/union/derive_no_fields.rs
deleted file mode 100644
index 4e4cb17db..000000000
--- a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-#[derive(juniper::GraphQLUnion)]
-enum Character {}
-
-fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr b/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr
deleted file mode 100644
index 5c069f6fa..000000000
--- a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr
+++ /dev/null
@@ -1,7 +0,0 @@
-error: GraphQL union expects at least one field
- --> $DIR/derive_no_fields.rs:2:1
-  |
-2 | enum Character {}
-  | ^^^^^^^^^^^^^^^^^
-  |
-  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr b/integration_tests/codegen_fail/fail/union/derive_same_type.stderr
deleted file mode 100644
index 6e62556e5..000000000
--- a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr
+++ /dev/null
@@ -1,10 +0,0 @@
-error[E0119]: conflicting implementations of trait `std::convert::From<std::string::String>` for type `Character`:
- --> $DIR/derive_same_type.rs:1:10
-  |
-1 | #[derive(juniper::GraphQLUnion)]
-  |          ^^^^^^^^^^^^^^^^^^^^^
-  |          |
-  |          first implementation here
-  |          conflicting implementation for `Character`
-  |
-  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs
new file mode 100644
index 000000000..174cd2ad1
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs
@@ -0,0 +1,6 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+union Character { id: i32 }
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr
new file mode 100644
index 000000000..fdeb15b40
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr
@@ -0,0 +1,5 @@
+error: GraphQL union can only be derived for enums and structs
+ --> $DIR/derive_wrong_item.rs:4:1
+  |
+4 | union Character { id: i32 }
+  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs
new file mode 100644
index 000000000..2da4466f6
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs
@@ -0,0 +1,15 @@
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLUnion)]
+#[graphql(on Human = resolve_fn1)]
+enum Character {
+    #[graphql(with = resolve_fn2)]
+    A(Human),
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr
new file mode 100644
index 000000000..12957f131
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union variant `Human` already has external resolver function `resolve_fn1` declared on the enum
+ --> $DIR/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs:6:15
+  |
+6 |     #[graphql(with = resolve_fn2)]
+  |               ^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs
new file mode 100644
index 000000000..2d0fa6902
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs
@@ -0,0 +1,13 @@
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLUnion)]
+enum __Character {
+    A(Human),
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr
new file mode 100644
index 000000000..df5db23fd
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr
@@ -0,0 +1,7 @@
+error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system.
+ --> $DIR/enum_name_double_underscored.rs:4:6
+  |
+4 | enum __Character {
+  |      ^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Schema
diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.rs b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs
new file mode 100644
index 000000000..66d35f020
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs
@@ -0,0 +1,6 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+enum Character {}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr
new file mode 100644
index 000000000..85ea25850
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union expects at least one union variant
+ --> $DIR/enum_no_fields.rs:4:1
+  |
+4 | enum Character {}
+  | ^^^^^^^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs
new file mode 100644
index 000000000..1019baf16
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs
@@ -0,0 +1,14 @@
+use juniper::{GraphQLEnum, GraphQLUnion};
+
+#[derive(GraphQLEnum)]
+pub enum Test {
+    A,
+    B,
+}
+
+#[derive(GraphQLUnion)]
+enum Character {
+    Test(Test),
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr
new file mode 100644
index 000000000..1da87bdd8
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr
@@ -0,0 +1,17 @@
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not satisfied
+ --> $DIR/enum_non_object_variant.rs:9:10
+  |
+9 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied
+ --> $DIR/enum_non_object_variant.rs:9:10
+  |
+9 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs
new file mode 100644
index 000000000..cbc1a12e0
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs
@@ -0,0 +1,9 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+enum Character {
+    A(u8),
+    B(u8),
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr
new file mode 100644
index 000000000..65cac9c36
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr
@@ -0,0 +1,10 @@
+error: GraphQL union must have a different type for each union variant
+ --> $DIR/enum_same_type_pretty.rs:4:1
+  |
+4 | / enum Character {
+5 | |     A(u8),
+6 | |     B(u8),
+7 | | }
+  | |_^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs
similarity index 59%
rename from integration_tests/codegen_fail/fail/union/derive_same_type.rs
rename to integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs
index e267a601b..28cf2af84 100644
--- a/integration_tests/codegen_fail/fail/union/derive_same_type.rs
+++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs
@@ -1,4 +1,6 @@
-#[derive(juniper::GraphQLUnion)]
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
 enum Character {
     A(std::string::String),
     B(String),
diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr
new file mode 100644
index 000000000..236daf7f2
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr
@@ -0,0 +1,10 @@
+error[E0119]: conflicting implementations of trait `<Character as juniper::types::marker::GraphQLUnion>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
+ --> $DIR/enum_same_type_ugly.rs:3:10
+  |
+3 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^
+  |          |
+  |          first implementation here
+  |          conflicting implementation for `std::string::String`
+  |
+  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs
new file mode 100644
index 000000000..84492eb88
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs
@@ -0,0 +1,18 @@
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLUnion)]
+enum Character1 {
+    A { human: Human },
+}
+
+#[derive(GraphQLUnion)]
+enum Character2 {
+    A(Human, u8),
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr
new file mode 100644
index 000000000..4f5def6a9
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr
@@ -0,0 +1,15 @@
+error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)`
+ --> $DIR/enum_wrong_variant_field.rs:5:5
+  |
+5 |     A { human: Human },
+  |     ^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
+
+error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)`
+  --> $DIR/enum_wrong_variant_field.rs:10:6
+   |
+10 |     A(Human, u8),
+   |      ^^^^^^^^^^^
+   |
+   = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs b/integration_tests/codegen_fail/fail/union/impl_enum_field.rs
deleted file mode 100644
index f3b67dd2b..000000000
--- a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-#[derive(juniper::GraphQLEnum)]
-#[graphql(context = ())]
-pub enum Test {
-    A,
-    B,
-}
-
-enum Character {
-    Test(Test),
-}
-
-#[juniper::graphql_union]
-impl Character {
-    fn resolve(&self) {
-        match self {
-            Test => match *self {
-                Character::Test(ref h) => Some(h),
-            },
-        }
-    }
-}
-
-fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr b/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr
deleted file mode 100644
index 0337b9985..000000000
--- a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr
+++ /dev/null
@@ -1,8 +0,0 @@
-error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not satisfied
-  --> $DIR/impl_enum_field.rs:12:1
-   |
-12 | #[juniper::graphql_union]
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not implemented for `Test`
-   |
-   = note: required by `juniper::types::marker::GraphQLObjectType::mark`
-   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/impl_no_fields.rs b/integration_tests/codegen_fail/fail/union/impl_no_fields.rs
deleted file mode 100644
index bafbe4778..000000000
--- a/integration_tests/codegen_fail/fail/union/impl_no_fields.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-enum Character {}
-
-#[juniper::graphql_union]
-impl Character {
-    fn resolve(&self) {
-        match self {}
-    }
-}
-
-fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr b/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr
deleted file mode 100644
index 7eb1e9c75..000000000
--- a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr
+++ /dev/null
@@ -1,9 +0,0 @@
-error: GraphQL union expects at least one field
- --> $DIR/impl_no_fields.rs:5:5
-  |
-5 | /     fn resolve(&self) {
-6 | |         match self {}
-7 | |     }
-  | |_____^
-  |
-  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled b/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled
deleted file mode 100644
index 4f070957f..000000000
--- a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled
+++ /dev/null
@@ -1,32 +0,0 @@
-// NOTICE: This can not be tested. Implementing Into<T> for each
-// variant is not possible since we did not created the
-// enum. Therefore, it is possible that the enum already has existing
-// Into<T> implementations.
-
-#[derive(juniper::GraphQLObject)]
-pub struct Test {
-    test: String,
-}
-
-enum Character {
-    A(Test),
-    B(Test),
-}
-
-#[juniper::graphql_union]
-impl Character {
-    fn resolve(&self) {
-        match self {
-            Test => match *self {
-                Character::A(ref h) => Some(h),
-                _ => None,
-            },
-            Test => match *self {
-                Character::B(ref h) => Some(h),
-                _ => None,
-            },
-        }
-    }
-}
-
-fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs
new file mode 100644
index 000000000..220bce55f
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs
@@ -0,0 +1,18 @@
+use juniper::{GraphQLObject, GraphQLUnion};
+
+#[derive(GraphQLUnion)]
+#[graphql(on Human = __Character::a)]
+struct __Character;
+
+impl __Character {
+    fn a(&self, _: &()) -> Option<&Human> {
+        None
+    }
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr
new file mode 100644
index 000000000..48054c637
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr
@@ -0,0 +1,7 @@
+error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system.
+ --> $DIR/struct_name_double_underscored.rs:5:8
+  |
+5 | struct __Character;
+  |        ^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Schema
diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.rs b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs
new file mode 100644
index 000000000..51fb88995
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs
@@ -0,0 +1,6 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+struct Character;
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr
new file mode 100644
index 000000000..3d6aaeb2b
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union expects at least one union variant
+ --> $DIR/struct_no_fields.rs:4:1
+  |
+4 | struct Character;
+  | ^^^^^^^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs
new file mode 100644
index 000000000..553cde13d
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs
@@ -0,0 +1,19 @@
+use juniper::{GraphQLEnum, GraphQLUnion};
+
+#[derive(GraphQLEnum)]
+pub enum Test {
+    A,
+    B,
+}
+
+#[derive(GraphQLUnion)]
+#[graphql(on Test = Character::a)]
+struct Character;
+
+impl Character {
+    fn a(&self, _: &()) -> Option<&Test> {
+        None
+    }
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr
new file mode 100644
index 000000000..53d7ad7e5
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr
@@ -0,0 +1,17 @@
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not satisfied
+ --> $DIR/struct_non_object_variant.rs:9:10
+  |
+9 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied
+ --> $DIR/struct_non_object_variant.rs:9:10
+  |
+9 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs
new file mode 100644
index 000000000..9df112110
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs
@@ -0,0 +1,18 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+#[graphql(on i32 = Character::a)]
+#[graphql(on i32 = Character::b)]
+struct Character;
+
+impl Character {
+    fn a(&self, _: &()) -> Option<&i32> {
+        None
+    }
+
+    fn b(&self, _: &()) -> Option<&i32> {
+        None
+    }
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr
new file mode 100644
index 000000000..fca94bfb8
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr
@@ -0,0 +1,5 @@
+error: duplicated attribute
+ --> $DIR/struct_same_type_pretty.rs:5:14
+  |
+5 | #[graphql(on i32 = Character::b)]
+  |              ^^^
diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs
new file mode 100644
index 000000000..b1467262c
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs
@@ -0,0 +1,18 @@
+use juniper::GraphQLUnion;
+
+#[derive(GraphQLUnion)]
+#[graphql(on String = Character::a)]
+#[graphql(on std::string::String = Character::b)]
+struct Character;
+
+impl Character {
+    fn a(&self, _: &()) -> Option<&String> {
+        None
+    }
+
+    fn b(&self, _: &()) -> Option<&String> {
+        None
+    }
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr
new file mode 100644
index 000000000..e901067c5
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr
@@ -0,0 +1,10 @@
+error[E0119]: conflicting implementations of trait `<Character as juniper::types::marker::GraphQLUnion>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
+ --> $DIR/struct_same_type_ugly.rs:3:10
+  |
+3 | #[derive(GraphQLUnion)]
+  |          ^^^^^^^^^^^^
+  |          |
+  |          first implementation here
+  |          conflicting implementation for `std::string::String`
+  |
+  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs
new file mode 100644
index 000000000..7077fe8c0
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs
@@ -0,0 +1,35 @@
+use juniper::{graphql_union, FromContext, GraphQLObject};
+
+#[graphql_union]
+trait Character {
+    fn a(&self, ctx: &SubContext) -> Option<&Human>;
+    fn b(&self, ctx: &CustomContext) -> Option<&Droid>;
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct Human {
+    id: String,
+    home_planet: String,
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct Droid {
+    id: String,
+    primary_function: String,
+}
+
+pub struct CustomContext;
+impl juniper::Context for CustomContext {}
+
+pub struct SubContext;
+impl juniper::Context for SubContext {}
+
+impl FromContext<CustomContext> for SubContext {
+    fn from(_: &CustomContext) -> &Self {
+        &Self
+    }
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr
new file mode 100644
index 000000000..6ffa46c57
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr
@@ -0,0 +1,18 @@
+error[E0277]: the trait bound `CustomContext: juniper::executor::FromContext<SubContext>` is not satisfied
+ --> $DIR/trait_fail_infer_context.rs:3:1
+  |
+3 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^ the trait `juniper::executor::FromContext<SubContext>` is not implemented for `CustomContext`
+  |
+  = note: required by `juniper::executor::FromContext::from`
+  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0308]: mismatched types
+ --> $DIR/trait_fail_infer_context.rs:3:1
+  |
+3 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^ expected struct `CustomContext`, found struct `SubContext`
+  |
+  = note: expected reference `&CustomContext`
+             found reference `&SubContext`
+  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs
new file mode 100644
index 000000000..aff4b1987
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs
@@ -0,0 +1,13 @@
+use juniper::{graphql_union, GraphQLObject};
+
+#[graphql_union(on Human = some_fn)]
+trait Character {
+    fn a(&self) -> Option<&Human>;
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr
new file mode 100644
index 000000000..0034185a0
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr
@@ -0,0 +1,8 @@
+error: GraphQL union trait method `a` conflicts with the external resolver function `some_fn` declared on the trait to resolve the variant type `Human`
+ --> $DIR/trait_method_conflicts_with_external_resolver_fn.rs:5:5
+  |
+5 |     fn a(&self) -> Option<&Human>;
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
+  = note: use `#[graphql_union(ignore)]` attribute to ignore this trait method for union variants resolution
diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs
new file mode 100644
index 000000000..86cb52258
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs
@@ -0,0 +1,13 @@
+use juniper::{graphql_union, GraphQLObject};
+
+#[graphql_union]
+trait __Character {
+    fn a(&self) -> Option<&Human>;
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr
new file mode 100644
index 000000000..d010fee27
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr
@@ -0,0 +1,7 @@
+error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQL’s introspection system.
+ --> $DIR/trait_name_double_underscored.rs:4:7
+  |
+4 | trait __Character {
+  |       ^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Schema
diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.rs b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs
new file mode 100644
index 000000000..a2b14cd2a
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs
@@ -0,0 +1,6 @@
+use juniper::graphql_union;
+
+#[graphql_union]
+trait Character {}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr
new file mode 100644
index 000000000..8261623dc
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union expects at least one union variant
+ --> $DIR/trait_no_fields.rs:4:1
+  |
+4 | trait Character {}
+  | ^^^^^^^^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs
new file mode 100644
index 000000000..4a1626b24
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs
@@ -0,0 +1,14 @@
+use juniper::{graphql_union, GraphQLEnum};
+
+#[derive(GraphQLEnum)]
+pub enum Test {
+    A,
+    B,
+}
+
+#[graphql_union]
+trait Character {
+    fn a(&self) -> Option<&Test>;
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr
new file mode 100644
index 000000000..98e0193b3
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr
@@ -0,0 +1,17 @@
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not satisfied
+ --> $DIR/trait_non_object_variant.rs:9:1
+  |
+9 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied
+ --> $DIR/trait_non_object_variant.rs:9:1
+  |
+9 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test`
+  |
+  = note: required by `juniper::types::marker::GraphQLObjectType::mark`
+  = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs
new file mode 100644
index 000000000..e84f5eb99
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs
@@ -0,0 +1,9 @@
+use juniper::graphql_union;
+
+#[graphql_union]
+trait Character {
+    fn a(&self) -> Option<&u8>;
+    fn b(&self) -> Option<&u8>;
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr
new file mode 100644
index 000000000..f899e3075
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr
@@ -0,0 +1,10 @@
+error: GraphQL union must have a different type for each union variant
+ --> $DIR/trait_same_type_pretty.rs:4:1
+  |
+4 | / trait Character {
+5 | |     fn a(&self) -> Option<&u8>;
+6 | |     fn b(&self) -> Option<&u8>;
+7 | | }
+  | |_^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs
new file mode 100644
index 000000000..b6274ad23
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs
@@ -0,0 +1,9 @@
+use juniper::graphql_union;
+
+#[graphql_union]
+trait Character {
+    fn a(&self) -> Option<&String>;
+    fn b(&self) -> Option<&std::string::String>;
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr
new file mode 100644
index 000000000..39684e76d
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr
@@ -0,0 +1,10 @@
+error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::types::marker::GraphQLUnion>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`:
+ --> $DIR/trait_same_type_ugly.rs:3:1
+  |
+3 | #[graphql_union]
+  | ^^^^^^^^^^^^^^^^
+  | |
+  | first implementation here
+  | conflicting implementation for `std::string::String`
+  |
+  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs
new file mode 100644
index 000000000..730c6a6da
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs
@@ -0,0 +1,14 @@
+use juniper::{graphql_union, GraphQLObject};
+
+#[graphql_union]
+trait Character {
+    #[graphql_union(with = something)]
+    fn a(&self) -> Option<&Human>;
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr
new file mode 100644
index 000000000..aba0ab2ef
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr
@@ -0,0 +1,8 @@
+error: GraphQL union cannot use #[graphql_union(with = ...)] attribute on a trait method
+ --> $DIR/trait_with_attr_on_method.rs:5:21
+  |
+5 |     #[graphql_union(with = something)]
+  |                     ^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
+  = note: instead use #[graphql_union(ignore)] on the method with #[graphql_union(on ... = ...)] on the trait itself
diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs
new file mode 100644
index 000000000..da9ef3b65
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs
@@ -0,0 +1,13 @@
+use juniper::{graphql_union, GraphQLObject};
+
+#[graphql_union]
+trait Character {
+    fn a(&self, ctx: &(), rand: u8) -> Option<&Human>;
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr
new file mode 100644
index 000000000..9b2fa2d85
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union expects trait method to accept `&self` only and, optionally, `&Context`
+ --> $DIR/trait_wrong_method_input_args.rs:5:10
+  |
+5 |     fn a(&self, ctx: &(), rand: u8) -> Option<&Human>;
+  |          ^^^^^^^^^^^^^^^^^^^^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs
new file mode 100644
index 000000000..b63f4353c
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs
@@ -0,0 +1,13 @@
+use juniper::{graphql_union, GraphQLObject};
+
+#[graphql_union]
+trait Character {
+    fn a(&self) -> &Human;
+}
+
+#[derive(GraphQLObject)]
+pub struct Human {
+    id: String,
+}
+
+fn main() {}
diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr
new file mode 100644
index 000000000..bbc40568a
--- /dev/null
+++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr
@@ -0,0 +1,7 @@
+error: GraphQL union expects trait method return type to be `Option<&VariantType>` only
+ --> $DIR/trait_wrong_method_return_type.rs:5:20
+  |
+5 |     fn a(&self) -> &Human;
+  |                    ^^^^^^
+  |
+  = note: https://spec.graphql.org/June2018/#sec-Unions
diff --git a/integration_tests/juniper_tests/Cargo.toml b/integration_tests/juniper_tests/Cargo.toml
index 719018127..893571478 100644
--- a/integration_tests/juniper_tests/Cargo.toml
+++ b/integration_tests/juniper_tests/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "juniper_tests"
 version = "0.1.0"
-publish = false
 edition = "2018"
+publish = false
 
 [dependencies]
-juniper = { path = "../../juniper" }
+derive_more = "0.99.7"
 futures = "0.3.1"
+juniper = { path = "../../juniper" }
 
 [dev-dependencies]
 serde_json = { version = "1" }
diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs
deleted file mode 100644
index fcec2e74c..000000000
--- a/integration_tests/juniper_tests/src/codegen/derive_union.rs
+++ /dev/null
@@ -1,275 +0,0 @@
-// Test for union's derive macro
-
-#[cfg(test)]
-use fnv::FnvHashMap;
-
-#[cfg(test)]
-use juniper::{
-    self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode,
-    Value, Variables,
-};
-
-#[derive(juniper::GraphQLObject)]
-pub struct Human {
-    id: String,
-    home_planet: String,
-}
-
-#[derive(juniper::GraphQLObject)]
-pub struct Droid {
-    id: String,
-    primary_function: String,
-}
-
-#[derive(juniper::GraphQLUnion)]
-#[graphql(description = "A Collection of things")]
-pub enum Character {
-    One(Human),
-    Two(Droid),
-}
-
-// Context Test
-pub struct CustomContext {
-    is_left: bool,
-}
-
-impl juniper::Context for CustomContext {}
-
-#[derive(juniper::GraphQLObject)]
-#[graphql(Context = CustomContext)]
-pub struct HumanContext {
-    id: String,
-    home_planet: String,
-}
-
-#[derive(juniper::GraphQLObject)]
-#[graphql(Context = CustomContext)]
-pub struct DroidContext {
-    id: String,
-    primary_function: String,
-}
-
-/// A Collection of things
-#[derive(juniper::GraphQLUnion)]
-#[graphql(Context = CustomContext)]
-pub enum CharacterContext {
-    One(HumanContext),
-    Two(DroidContext),
-}
-
-// #[juniper::object] compatibility
-
-pub struct HumanCompat {
-    id: String,
-    home_planet: String,
-}
-
-#[juniper::graphql_object]
-impl HumanCompat {
-    fn id(&self) -> &String {
-        &self.id
-    }
-
-    fn home_planet(&self) -> &String {
-        &self.home_planet
-    }
-}
-
-pub struct DroidCompat {
-    id: String,
-    primary_function: String,
-}
-
-#[juniper::graphql_object]
-impl DroidCompat {
-    fn id(&self) -> &String {
-        &self.id
-    }
-
-    fn primary_function(&self) -> &String {
-        &self.primary_function
-    }
-}
-
-#[derive(juniper::GraphQLUnion)]
-#[graphql(Context = CustomContext)]
-pub enum DifferentContext {
-    A(DroidContext),
-    B(Droid),
-}
-
-// NOTICE: this can not compile due to generic implementation of GraphQLType<__S>
-// #[derive(juniper::GraphQLUnion)]
-// pub enum CharacterCompatFail {
-//     One(HumanCompat),
-//     Two(DroidCompat),
-// }
-
-/// A Collection of things
-#[derive(juniper::GraphQLUnion)]
-#[graphql(Scalar = juniper::DefaultScalarValue)]
-pub enum CharacterCompat {
-    One(HumanCompat),
-    Two(DroidCompat),
-}
-
-pub struct Query;
-
-#[juniper::graphql_object(
-    Context = CustomContext,
-)]
-impl Query {
-    fn context(&self, ctx: &CustomContext) -> CharacterContext {
-        if ctx.is_left {
-            HumanContext {
-                id: "human-32".to_string(),
-                home_planet: "earth".to_string(),
-            }
-            .into()
-        } else {
-            DroidContext {
-                id: "droid-99".to_string(),
-                primary_function: "run".to_string(),
-            }
-            .into()
-        }
-    }
-}
-
-#[tokio::test]
-async fn test_derived_union_doc_macro() {
-    assert_eq!(
-        <Character as GraphQLType<DefaultScalarValue>>::name(&()),
-        Some("Character")
-    );
-
-    let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
-    let meta = Character::meta(&(), &mut registry);
-
-    assert_eq!(meta.name(), Some("Character"));
-    assert_eq!(
-        meta.description(),
-        Some(&"A Collection of things".to_string())
-    );
-}
-
-#[tokio::test]
-async fn test_derived_union_doc_string() {
-    assert_eq!(
-        <CharacterContext as GraphQLType<DefaultScalarValue>>::name(&()),
-        Some("CharacterContext")
-    );
-
-    let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
-    let meta = CharacterContext::meta(&(), &mut registry);
-
-    assert_eq!(meta.name(), Some("CharacterContext"));
-    assert_eq!(
-        meta.description(),
-        Some(&"A Collection of things".to_string())
-    );
-}
-
-#[tokio::test]
-async fn test_derived_union_left() {
-    let doc = r#"
-        {
-            context {
-                ... on HumanContext {
-                    humanId: id
-                    homePlanet
-                }
-                ... on DroidContext {
-                    droidId: id
-                    primaryFunction
-                }
-            }
-        }"#;
-
-    let schema = RootNode::new(
-        Query,
-        EmptyMutation::<CustomContext>::new(),
-        EmptySubscription::<CustomContext>::new(),
-    );
-
-    assert_eq!(
-        execute(
-            doc,
-            None,
-            &schema,
-            &Variables::new(),
-            &CustomContext { is_left: true }
-        )
-        .await,
-        Ok((
-            Value::object(
-                vec![(
-                    "context",
-                    Value::object(
-                        vec![
-                            ("humanId", Value::scalar("human-32".to_string())),
-                            ("homePlanet", Value::scalar("earth".to_string())),
-                        ]
-                        .into_iter()
-                        .collect(),
-                    ),
-                )]
-                .into_iter()
-                .collect()
-            ),
-            vec![]
-        ))
-    );
-}
-
-#[tokio::test]
-async fn test_derived_union_right() {
-    let doc = r#"
-        {
-            context {
-                ... on HumanContext {
-                    humanId: id
-                    homePlanet
-                }
-                ... on DroidContext {
-                    droidId: id
-                    primaryFunction
-                }
-            }
-        }"#;
-
-    let schema = RootNode::new(
-        Query,
-        EmptyMutation::<CustomContext>::new(),
-        EmptySubscription::<CustomContext>::new(),
-    );
-
-    assert_eq!(
-        execute(
-            doc,
-            None,
-            &schema,
-            &Variables::new(),
-            &CustomContext { is_left: false }
-        )
-        .await,
-        Ok((
-            Value::object(
-                vec![(
-                    "context",
-                    Value::object(
-                        vec![
-                            ("droidId", Value::scalar("droid-99".to_string())),
-                            ("primaryFunction", Value::scalar("run".to_string())),
-                        ]
-                        .into_iter()
-                        .collect(),
-                    ),
-                )]
-                .into_iter()
-                .collect()
-            ),
-            vec![]
-        ))
-    );
-}
diff --git a/integration_tests/juniper_tests/src/codegen/impl_union.rs b/integration_tests/juniper_tests/src/codegen/impl_union.rs
deleted file mode 100644
index 5ed28a3f4..000000000
--- a/integration_tests/juniper_tests/src/codegen/impl_union.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Trait.
-
-#[derive(juniper::GraphQLObject)]
-struct Human {
-    id: String,
-    home_planet: String,
-}
-
-#[derive(juniper::GraphQLObject)]
-struct Droid {
-    id: String,
-    primary_function: String,
-}
-
-trait Character {
-    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)
-    }
-}
-
-#[juniper::graphql_union]
-impl<'a> GraphQLUnion for &'a dyn Character {
-    fn resolve(&self) {
-        match self {
-            Human => self.as_human(),
-            Droid => self.as_droid(),
-        }
-    }
-}
diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs
index f2bd68dc0..99d2b3651 100644
--- a/integration_tests/juniper_tests/src/codegen/mod.rs
+++ b/integration_tests/juniper_tests/src/codegen/mod.rs
@@ -2,8 +2,8 @@ mod derive_enum;
 mod derive_input_object;
 mod derive_object;
 mod derive_object_with_raw_idents;
-mod derive_union;
 mod impl_object;
 mod impl_scalar;
-mod impl_union;
 mod scalar_value_transparent;
+mod union_attr;
+mod union_derive;
diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs
new file mode 100644
index 000000000..7377dbea3
--- /dev/null
+++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs
@@ -0,0 +1,1077 @@
+//! Tests for `#[graphql_union]` macro.
+
+use juniper::{
+    execute, graphql_object, graphql_union, graphql_value, DefaultScalarValue, EmptyMutation,
+    EmptySubscription, GraphQLObject, GraphQLType, RootNode, ScalarValue, Variables,
+};
+
+#[derive(GraphQLObject)]
+struct Human {
+    id: String,
+    home_planet: String,
+}
+
+#[derive(GraphQLObject)]
+struct Droid {
+    id: String,
+    primary_function: String,
+}
+
+#[derive(GraphQLObject)]
+struct Ewok {
+    id: String,
+    funny: bool,
+}
+
+pub enum CustomContext {
+    Human,
+    Droid,
+    Ewok,
+}
+impl juniper::Context for CustomContext {}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct HumanCustomContext {
+    id: String,
+    home_planet: String,
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct DroidCustomContext {
+    id: String,
+    primary_function: String,
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+struct EwokCustomContext {
+    id: String,
+    funny: bool,
+}
+
+fn schema<'q, C, S, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>, S>
+where
+    Q: GraphQLType<S, Context = C, TypeInfo = ()> + 'q,
+    S: ScalarValue + 'q,
+{
+    RootNode::new(
+        query_root,
+        EmptyMutation::<C>::new(),
+        EmptySubscription::<C>::new(),
+    )
+}
+
+mod trivial {
+    use super::*;
+
+    #[graphql_union]
+    trait Character {
+        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)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            let ch: Box<DynCharacter<'_>> = match self {
+                Self::Human => Box::new(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Box::new(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn is_graphql_union() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                kind
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"kind": "UNION"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_type_name() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn has_no_description() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"description": None}}), vec![])),
+        );
+    }
+}
+
+mod generic {
+    use super::*;
+
+    #[graphql_union]
+    trait Character<A, B> {
+        fn as_human(&self) -> Option<&Human> {
+            None
+        }
+        fn as_droid(&self) -> Option<&Droid> {
+            None
+        }
+    }
+
+    impl<A, B> Character<A, B> for Human {
+        fn as_human(&self) -> Option<&Human> {
+            Some(&self)
+        }
+    }
+
+    impl<A, B> Character<A, B> for Droid {
+        fn as_droid(&self) -> Option<&Droid> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a, A, B> = dyn Character<A, B> + Send + Sync + 'a;
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_, u8, ()>> {
+            let ch: Box<DynCharacter<'_, u8, ()>> = match self {
+                Self::Human => Box::new(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Box::new(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_type_name_without_type_params() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])),
+        );
+    }
+}
+
+mod description_from_doc_comments {
+    use super::*;
+
+    /// Rust docs.
+    #[graphql_union]
+    trait Character {
+        fn as_human(&self) -> Option<&Human> {
+            None
+        }
+    }
+
+    impl Character for Human {
+        fn as_human(&self) -> Option<&Human> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            Box::new(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_doc_comment_as_description() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"description": "Rust docs."}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod explicit_name_and_description {
+    use super::*;
+
+    /// Rust docs.
+    #[graphql_union(name = "MyChar", desc = "My character.")]
+    trait Character {
+        fn as_human(&self) -> Option<&Human> {
+            None
+        }
+    }
+
+    impl Character for Human {
+        fn as_human(&self) -> Option<&Human> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            Box::new(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_name() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_description() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"description": "My character."}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod explicit_scalar {
+    use super::*;
+
+    #[graphql_union(scalar = DefaultScalarValue)]
+    trait Character {
+        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)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(scalar = DefaultScalarValue)]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            let ch: Box<DynCharacter<'_>> = match self {
+                Self::Human => Box::new(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Box::new(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod inferred_custom_context {
+    use super::*;
+
+    #[graphql_union]
+    trait Character {
+        fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
+            None
+        }
+        fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
+            None
+        }
+    }
+
+    impl Character for HumanCustomContext {
+        fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> {
+            Some(&self)
+        }
+    }
+
+    impl Character for DroidCustomContext {
+        fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Box<DynCharacter<'_>> {
+            let ch: Box<DynCharacter<'_>> = match ctx {
+                CustomContext::Human => Box::new(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Box::new(DroidCustomContext {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                _ => unimplemented!(),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on DroidCustomContext {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod explicit_custom_context {
+    use super::*;
+
+    #[graphql_union(context = CustomContext)]
+    trait Character {
+        fn as_human(&self) -> Option<&HumanCustomContext> {
+            None
+        }
+        fn as_droid(&self) -> Option<&DroidCustomContext> {
+            None
+        }
+    }
+
+    impl Character for HumanCustomContext {
+        fn as_human(&self) -> Option<&HumanCustomContext> {
+            Some(&self)
+        }
+    }
+
+    impl Character for DroidCustomContext {
+        fn as_droid(&self) -> Option<&DroidCustomContext> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Box<DynCharacter<'_>> {
+            let ch: Box<DynCharacter<'_>> = match ctx {
+                CustomContext::Human => Box::new(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Box::new(DroidCustomContext {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                _ => unimplemented!(),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on DroidCustomContext {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod ignored_methods {
+    use super::*;
+
+    #[graphql_union]
+    trait Character {
+        fn as_human(&self) -> Option<&Human> {
+            None
+        }
+        #[graphql_union(ignore)]
+        fn ignored(&self) -> Option<&Ewok> {
+            None
+        }
+        #[graphql_union(skip)]
+        fn skipped(&self) {}
+    }
+
+    impl Character for Human {
+        fn as_human(&self) -> Option<&Human> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            Box::new(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn ignores_ewok() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                possibleTypes {
+                    name
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod external_resolver {
+    use super::*;
+
+    #[graphql_union(context = Database)]
+    #[graphql_union(on Droid = DynCharacter::as_droid)]
+    trait Character {
+        fn as_human(&self) -> Option<&Human> {
+            None
+        }
+    }
+
+    impl Character for Human {
+        fn as_human(&self) -> Option<&Human> {
+            Some(&self)
+        }
+    }
+
+    impl Character for Droid {}
+
+    type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
+
+    impl<'a> DynCharacter<'a> {
+        fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> {
+            db.droid.as_ref()
+        }
+    }
+
+    struct Database {
+        droid: Option<Droid>,
+    }
+    impl juniper::Context for Database {}
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Box<DynCharacter<'_>> {
+            let ch: Box<DynCharacter<'_>> = match self {
+                Self::Human => Box::new(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Box::new(Droid {
+                    id: "?????".to_string(),
+                    primary_function: "???".to_string(),
+                }),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+        let db = Database { droid: None };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+        let db = Database {
+            droid: Some(Droid {
+                id: "droid-99".to_string(),
+                primary_function: "run".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod full_featured {
+    use super::*;
+
+    /// Rust doc.
+    #[graphql_union(name = "MyChar")]
+    #[graphql_union(description = "My character.")]
+    #[graphql_union(context = CustomContext, scalar = DefaultScalarValue)]
+    #[graphql_union(on EwokCustomContext = resolve_ewok)]
+    trait Character<T> {
+        fn as_human(&self, _: &()) -> Option<&HumanCustomContext> {
+            None
+        }
+        fn as_droid(&self) -> Option<&DroidCustomContext> {
+            None
+        }
+        #[graphql_union(ignore)]
+        fn as_ewok(&self) -> Option<&EwokCustomContext> {
+            None
+        }
+        #[graphql_union(ignore)]
+        fn ignored(&self) {}
+    }
+
+    impl<T> Character<T> for HumanCustomContext {
+        fn as_human(&self, _: &()) -> Option<&HumanCustomContext> {
+            Some(&self)
+        }
+    }
+
+    impl<T> Character<T> for DroidCustomContext {
+        fn as_droid(&self) -> Option<&DroidCustomContext> {
+            Some(&self)
+        }
+    }
+
+    impl<T> Character<T> for EwokCustomContext {
+        fn as_ewok(&self) -> Option<&EwokCustomContext> {
+            Some(&self)
+        }
+    }
+
+    type DynCharacter<'a, T> = dyn Character<T> + Send + Sync + 'a;
+
+    fn resolve_ewok<'a, T>(
+        ewok: &'a DynCharacter<'a, T>,
+        _: &CustomContext,
+    ) -> Option<&'a EwokCustomContext> {
+        ewok.as_ewok()
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Box<DynCharacter<'_, ()>> {
+            let ch: Box<DynCharacter<'_, ()>> = match ctx {
+                CustomContext::Human => Box::new(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Box::new(DroidCustomContext {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                CustomContext::Ewok => Box::new(EwokCustomContext {
+                    id: "ewok-1".to_string(),
+                    funny: true,
+                }),
+            };
+            ch
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on DroidCustomContext {
+                droidId: id
+                primaryFunction
+            }
+            ... on EwokCustomContext {
+                ewokId: id
+                funny
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_ewok() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((
+                graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_name() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_description() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((
+                graphql_value!({"__type": {"description": "My character."}}),
+                vec![],
+            )),
+        );
+    }
+}
diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs
new file mode 100644
index 000000000..55d4c047a
--- /dev/null
+++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs
@@ -0,0 +1,1388 @@
+//! Tests for `#[derive(GraphQLUnion)]` macro.
+
+use std::marker::PhantomData;
+
+use juniper::{
+    execute, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription,
+    GraphQLObject, GraphQLType, GraphQLUnion, RootNode, ScalarValue, Variables,
+};
+
+#[derive(GraphQLObject)]
+struct Human {
+    id: String,
+    home_planet: String,
+}
+
+#[derive(GraphQLObject)]
+struct Droid {
+    id: String,
+    primary_function: String,
+}
+
+#[derive(GraphQLObject)]
+struct Ewok {
+    id: String,
+    funny: bool,
+}
+
+pub enum CustomContext {
+    Human,
+    Droid,
+    Ewok,
+}
+impl juniper::Context for CustomContext {}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct HumanCustomContext {
+    id: String,
+    home_planet: String,
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+pub struct DroidCustomContext {
+    id: String,
+    primary_function: String,
+}
+
+#[derive(GraphQLObject)]
+#[graphql(context = CustomContext)]
+struct EwokCustomContext {
+    id: String,
+    funny: bool,
+}
+
+fn schema<'q, C, S, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>, S>
+where
+    Q: GraphQLType<S, Context = C, TypeInfo = ()> + 'q,
+    S: ScalarValue + 'q,
+{
+    RootNode::new(
+        query_root,
+        EmptyMutation::<C>::new(),
+        EmptySubscription::<C>::new(),
+    )
+}
+
+mod trivial_enum {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    enum Character {
+        A(Human),
+        B(Droid),
+    }
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            match self {
+                Self::Human => Character::A(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Character::B(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn is_graphql_union() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                kind
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"kind": "UNION"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_type_name() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn has_no_description() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"description": None}}), vec![])),
+        );
+    }
+}
+
+mod generic_enum {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    enum Character<A, B> {
+        A(Human),
+        B(Droid),
+        #[graphql(ignore)]
+        _State(A, B),
+    }
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character<u8, ()> {
+            match self {
+                Self::Human => Character::A(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Character::B(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_type_name_without_type_params() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])),
+        );
+    }
+}
+
+mod description_from_doc_comments {
+    use super::*;
+
+    /// Rust docs.
+    #[derive(GraphQLUnion)]
+    enum Character {
+        A(Human),
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            Character::A(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_doc_comment_as_description() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"description": "Rust docs."}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod explicit_name_and_description {
+    use super::*;
+
+    /// Rust docs.
+    #[derive(GraphQLUnion)]
+    #[graphql(name = "MyChar", desc = "My character.")]
+    enum Character {
+        A(Human),
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            Character::A(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_name() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_description() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"description": "My character."}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod explicit_scalar {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(scalar = DefaultScalarValue)]
+    enum Character {
+        A(Human),
+        B(Droid),
+    }
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            match self {
+                Self::Human => Character::A(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Character::B(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Human);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Droid);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod custom_context {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = CustomContext)]
+    enum Character {
+        A(HumanCustomContext),
+        B(DroidCustomContext),
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Character {
+            match ctx {
+                CustomContext::Human => Character::A(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Character::B(DroidCustomContext {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on DroidCustomContext {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod different_context {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = CustomContext)]
+    enum Character {
+        A(HumanCustomContext),
+        B(Droid),
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Character {
+            match ctx {
+                CustomContext::Human => Character::A(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Character::B(Droid {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod ignored_enum_variants {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    enum Character {
+        A(Human),
+        #[graphql(ignore)]
+        _C(Ewok),
+        #[graphql(skip)]
+        _D,
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            Character::A(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            })
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn ignores_ewok() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                possibleTypes {
+                    name
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &()).await,
+            Ok((
+                graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod external_resolver_enum {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = Database)]
+    #[graphql(on Droid = Character::as_droid)]
+    enum Character {
+        A(Human),
+        #[graphql(ignore)]
+        B,
+    }
+
+    impl Character {
+        fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> {
+            if let Self::B = self {
+                db.droid.as_ref()
+            } else {
+                None
+            }
+        }
+    }
+
+    struct Database {
+        droid: Option<Droid>,
+    }
+    impl juniper::Context for Database {}
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            match self {
+                Self::Human => Character::A(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Character::B,
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+        let db = Database { droid: None };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+        let db = Database {
+            droid: Some(Droid {
+                id: "droid-99".to_string(),
+                primary_function: "run".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod external_resolver_enum_variant {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = Database)]
+    enum Character {
+        A(Human),
+        #[graphql(with = Character::as_droid)]
+        B(Droid),
+    }
+
+    impl Character {
+        fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> {
+            if let Self::B(_) = self {
+                db.droid.as_ref()
+            } else {
+                None
+            }
+        }
+    }
+
+    struct Database {
+        droid: Option<Droid>,
+    }
+    impl juniper::Context for Database {}
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            match self {
+                Self::Human => Character::A(Human {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                Self::Droid => Character::B(Droid {
+                    id: "?????".to_string(),
+                    primary_function: "???".to_string(),
+                }),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+        let db = Database { droid: None };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+        let db = Database {
+            droid: Some(Droid {
+                id: "droid-99".to_string(),
+                primary_function: "run".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod full_featured_enum {
+    use super::*;
+
+    /// Rust doc.
+    #[derive(GraphQLUnion)]
+    #[graphql(name = "MyChar")]
+    #[graphql(description = "My character.")]
+    #[graphql(context = CustomContext, scalar = DefaultScalarValue)]
+    #[graphql(on EwokCustomContext = resolve_ewok)]
+    enum Character<T> {
+        A(HumanCustomContext),
+        #[graphql(with = Character::as_droid)]
+        B(DroidCustomContext),
+        #[graphql(ignore)]
+        C(EwokCustomContext),
+        #[graphql(ignore)]
+        _State(T),
+    }
+
+    impl<T> Character<T> {
+        fn as_droid(&self, ctx: &CustomContext) -> Option<&DroidCustomContext> {
+            if let CustomContext::Droid = ctx {
+                if let Self::B(droid) = self {
+                    return Some(droid);
+                }
+            }
+            None
+        }
+    }
+
+    fn resolve_ewok<'a, T>(
+        ewok: &'a Character<T>,
+        _: &CustomContext,
+    ) -> Option<&'a EwokCustomContext> {
+        if let Character::C(ewok) = ewok {
+            Some(ewok)
+        } else {
+            None
+        }
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object(context = CustomContext)]
+    impl QueryRoot {
+        fn character(&self, ctx: &CustomContext) -> Character<()> {
+            match ctx {
+                CustomContext::Human => Character::A(HumanCustomContext {
+                    id: "human-32".to_string(),
+                    home_planet: "earth".to_string(),
+                }),
+                CustomContext::Droid => Character::B(DroidCustomContext {
+                    id: "droid-99".to_string(),
+                    primary_function: "run".to_string(),
+                }),
+                CustomContext::Ewok => Character::C(EwokCustomContext {
+                    id: "ewok-1".to_string(),
+                    funny: true,
+                }),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on HumanCustomContext {
+                humanId: id
+                homePlanet
+            }
+            ... on DroidCustomContext {
+                droidId: id
+                primaryFunction
+            }
+            ... on EwokCustomContext {
+                ewokId: id
+                funny
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_ewok() {
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((
+                graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_name() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_description() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await,
+            Ok((
+                graphql_value!({"__type": {"description": "My character."}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod trivial_struct {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = Database)]
+    #[graphql(
+    on Human = Character::as_human,
+    on Droid = Character::as_droid,
+    )]
+    struct Character {
+        id: String,
+    }
+
+    impl Character {
+        fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> {
+            if let Some(human) = &db.human {
+                if human.id == self.id {
+                    return Some(human);
+                }
+            }
+            None
+        }
+
+        fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> {
+            if let Some(droid) = &db.droid {
+                if droid.id == self.id {
+                    return Some(droid);
+                }
+            }
+            None
+        }
+    }
+
+    struct Database {
+        human: Option<Human>,
+        droid: Option<Droid>,
+    }
+    impl juniper::Context for Database {}
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Character {
+            Character {
+                id: match self {
+                    Self::Human => "human-32",
+                    Self::Droid => "droid-99",
+                }
+                .to_string(),
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+        let db = Database {
+            human: Some(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            }),
+            droid: None,
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+        let db = Database {
+            human: None,
+            droid: Some(Droid {
+                id: "droid-99".to_string(),
+                primary_function: "run".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod generic_struct {
+    use super::*;
+
+    #[derive(GraphQLUnion)]
+    #[graphql(context = Database)]
+    #[graphql(on Human = Character::as_human)]
+    struct Character<A, B> {
+        id: String,
+        _s: PhantomData<(A, B)>,
+    }
+
+    impl<A, B> Character<A, B> {
+        fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> {
+            if let Some(human) = &db.human {
+                if human.id == self.id {
+                    return Some(human);
+                }
+            }
+            None
+        }
+    }
+
+    struct Database {
+        human: Option<Human>,
+    }
+    impl juniper::Context for Database {}
+
+    struct QueryRoot;
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Character<u8, ()> {
+            Character {
+                id: "human-32".to_string(),
+                _s: PhantomData,
+            }
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves_human() {
+        const DOC: &str = r#"{
+            character {
+                ... on Human {
+                    humanId: id
+                    homePlanet
+                }
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+        let db = Database {
+            human: Some(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_type_name_without_type_params() {
+        const DOC: &str = r#"{
+            __type(name: "Character") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+        let db = Database { human: None };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])),
+        );
+    }
+}
+
+mod full_featured_struct {
+    use super::*;
+
+    /// Rust doc.
+    #[derive(GraphQLUnion)]
+    #[graphql(name = "MyChar")]
+    #[graphql(description = "My character.")]
+    #[graphql(context = Database, scalar = DefaultScalarValue)]
+    #[graphql(on Human = Character::as_human)]
+    #[graphql(on Droid = Character::as_droid)]
+    struct Character<T> {
+        id: String,
+        _s: PhantomData<T>,
+    }
+
+    impl<T> Character<T> {
+        fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> {
+            if let Some(human) = &db.human {
+                if human.id == self.id {
+                    return Some(human);
+                }
+            }
+            None
+        }
+    }
+
+    impl<T> Character<T> {
+        fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> {
+            if let Some(droid) = &db.droid {
+                if droid.id == self.id {
+                    return Some(droid);
+                }
+            }
+            None
+        }
+    }
+
+    struct Database {
+        human: Option<Human>,
+        droid: Option<Droid>,
+    }
+    impl juniper::Context for Database {}
+
+    enum QueryRoot {
+        Human,
+        Droid,
+    }
+
+    #[graphql_object(context = Database)]
+    impl QueryRoot {
+        fn character(&self) -> Character<()> {
+            Character {
+                id: match self {
+                    Self::Human => "human-32",
+                    Self::Droid => "droid-99",
+                }
+                .to_string(),
+                _s: PhantomData,
+            }
+        }
+    }
+
+    const DOC: &str = r#"{
+        character {
+            ... on Human {
+                humanId: id
+                homePlanet
+            }
+            ... on Droid {
+                droidId: id
+                primaryFunction
+            }
+        }
+    }"#;
+
+    #[tokio::test]
+    async fn resolves_human() {
+        let schema = schema(QueryRoot::Human);
+        let db = Database {
+            human: Some(Human {
+                id: "human-32".to_string(),
+                home_planet: "earth".to_string(),
+            }),
+            droid: None,
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn resolves_droid() {
+        let schema = schema(QueryRoot::Droid);
+        let db = Database {
+            human: None,
+            droid: Some(Droid {
+                id: "droid-99".to_string(),
+                primary_function: "run".to_string(),
+            }),
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_name() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                name
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+        let db = Database {
+            human: None,
+            droid: None,
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn uses_custom_description() {
+        const DOC: &str = r#"{
+            __type(name: "MyChar") {
+                description
+            }
+        }"#;
+
+        let schema = schema(QueryRoot::Human);
+        let db = Database {
+            human: None,
+            droid: None,
+        };
+
+        assert_eq!(
+            execute(DOC, None, &schema, &Variables::new(), &db).await,
+            Ok((
+                graphql_value!({"__type": {"description": "My character."}}),
+                vec![],
+            )),
+        );
+    }
+}
diff --git a/juniper/src/executor_tests/interfaces_unions.rs b/juniper/src/executor_tests/interfaces_unions.rs
index 51a17ed9f..54fcad306 100644
--- a/juniper/src/executor_tests/interfaces_unions.rs
+++ b/juniper/src/executor_tests/interfaces_unions.rs
@@ -167,6 +167,7 @@ mod union {
         value::Value,
     };
 
+    #[crate::graphql_union_internal]
     trait Pet {
         fn as_dog(&self) -> Option<&Dog> {
             None
@@ -176,16 +177,6 @@ mod union {
         }
     }
 
-    #[crate::graphql_union_internal]
-    impl<'a> GraphQLUnion for &'a dyn Pet {
-        fn resolve(&self) {
-            match self {
-                Dog => self.as_dog(),
-                Cat => self.as_cat(),
-            }
-        }
-    }
-
     struct Dog {
         name: String,
         woofs: bool,
diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs
index 6dfbfe754..9db321eef 100644
--- a/juniper/src/macros/tests/union.rs
+++ b/juniper/src/macros/tests/union.rs
@@ -14,81 +14,70 @@ use std::marker::PhantomData;
 
 use crate::{
     ast::InputValue,
+    graphql_object_internal,
     schema::model::RootNode,
     types::scalars::{EmptyMutation, EmptySubscription},
     value::{DefaultScalarValue, Object, Value},
+    GraphQLUnionInternal,
 };
 
 struct Concrete;
 
+#[graphql_object_internal]
+impl Concrete {
+    fn simple() -> i32 {
+        123
+    }
+}
+
+#[derive(GraphQLUnionInternal)]
+#[graphql(name = "ACustomNamedUnion", scalar = DefaultScalarValue)]
 enum CustomName {
     Concrete(Concrete),
 }
 
+#[derive(GraphQLUnionInternal)]
+#[graphql(on Concrete = WithLifetime::resolve, scalar = DefaultScalarValue)]
 enum WithLifetime<'a> {
+    #[graphql(ignore)]
     Int(PhantomData<&'a i32>),
 }
-enum WithGenerics<T> {
-    Generic(T),
-}
 
-enum DescriptionFirst {
-    Concrete(Concrete),
-}
-
-struct Root;
-
-#[crate::graphql_object_internal]
-impl Concrete {
-    fn simple() -> i32 {
-        123
-    }
-}
-
-#[crate::graphql_union_internal(name = "ACustomNamedUnion")]
-impl CustomName {
-    fn resolve(&self) {
-        match self {
-            Concrete => match *self {
-                CustomName::Concrete(ref c) => Some(c),
-            },
+impl<'a> WithLifetime<'a> {
+    fn resolve(&self, _: &()) -> Option<&Concrete> {
+        if matches!(self, Self::Int(_)) {
+            Some(&Concrete)
+        } else {
+            None
         }
     }
 }
 
-#[crate::graphql_union_internal]
-impl<'a> WithLifetime<'a> {
-    fn resolve(&self) {
-        match self {
-            Concrete => match *self {
-                WithLifetime::Int(_) => Some(&Concrete),
-            },
-        }
-    }
+#[derive(GraphQLUnionInternal)]
+#[graphql(on Concrete = WithGenerics::resolve, scalar = DefaultScalarValue)]
+enum WithGenerics<T> {
+    #[graphql(ignore)]
+    Generic(T),
 }
 
-#[crate::graphql_union_internal]
 impl<T> WithGenerics<T> {
-    fn resolve(&self) {
-        match self {
-            Concrete => match *self {
-                WithGenerics::Generic(_) => Some(&Concrete),
-            },
+    fn resolve(&self, _: &()) -> Option<&Concrete> {
+        if matches!(self, Self::Generic(_)) {
+            Some(&Concrete)
+        } else {
+            None
         }
     }
 }
 
-#[crate::graphql_union_internal(description = "A description")]
-impl DescriptionFirst {
-    fn resolve(&self) {
-        match self {
-            Concrete => match *self {
-                DescriptionFirst::Concrete(ref c) => Some(c),
-            },
-        }
-    }
+#[derive(GraphQLUnionInternal)]
+#[graphql(description = "A description", scalar = DefaultScalarValue)]
+enum DescriptionFirst {
+    Concrete(Concrete),
 }
 
+struct Root;
+
 // FIXME: make async work
 #[crate::graphql_object_internal(noasync)]
 impl<'a> Root {