Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jbellenger/72544c2306b52bb94e5459b910f2097e to your computer and use it in GitHub Desktop.

Select an option

Save jbellenger/72544c2306b52bb94e5459b910f2097e to your computer and use it in GitHub Desktop.
async-graphql bug: inline fragment with union type condition silently drops fields

async-graphql: Inline fragment with union type condition fails to match

Summary

When a query uses an inline fragment with a union type condition (e.g. ... on MyUnion), and the current runtime type is a member of that union, async-graphql fails to spread the fragment. The fields inside the fragment are silently dropped, resulting in empty data.

This affects both the dynamic schema API and macro-derived types.

Minimal reproduction

# Schema
union Things = Query | Other

type Other {
  value: String
}

type Query {
  hello: String
}
# Query
{
  ... on Things {
    hello
  }
}

Expected: {"data": {"hello": "world"}} Actual: {"data": {}}

The inline fragment ... on Things should match because the runtime type (Query) is a member of Things. graphql-js, graphql-java, and hot-chocolate all return the expected result.

Reproduction using dynamic schema API

use async_graphql::dynamic::*;

#[tokio::test]
async fn inline_fragment_on_union_at_query_root() {
    let query = Object::new("Query").field(
        Field::new("hello", TypeRef::Named("String".into()), |_| {
            FieldFuture::new(async { Ok(Some(FieldValue::value("world"))) })
        }),
    );

    let other = Object::new("Other").field(
        Field::new("value", TypeRef::Named("String".into()), |_| {
            FieldFuture::new(async { Ok(Some(FieldValue::value("x"))) })
        }),
    );

    let things = Union::new("Things")
        .possible_type("Query")
        .possible_type("Other");

    let schema = Schema::build("Query", None, None)
        .register(query)
        .register(other)
        .register(things)
        .finish()
        .unwrap();

    let resp = schema.execute("{ ... on Things { hello } }").await;
    let data = resp.data.into_json().unwrap();

    // FAILS: data is {} instead of {"hello": "world"}
    assert_eq!(data, serde_json::json!({"hello": "world"}));
}

Root cause

In src/dynamic/resolve.rs around line 376, the type condition matching logic for inline fragments is:

let type_condition_matched = match type_condition {
    None => true,
    Some(type_condition) if type_condition == introspection_type_name => true,
    Some(type_condition) if object.implements.contains(type_condition) => true,
    _ => false,
};

This handles:

  1. No type condition → matches
  2. Type condition equals the concrete type name → matches
  3. Type condition is an interface the object implements → matches

Missing: type condition is a union that contains the concrete type. An object doesn't "implement" a union, so object.implements.contains("Things") is false, and the fragment is rejected.

The same bug exists in src/resolver_utils/container.rs around line 362, which handles macro-derived types.

Suggested fix

The registry already provides MetaType::is_possible_type() which checks both union membership and interface implementation:

// In src/dynamic/resolve.rs
let type_condition_matched = match type_condition {
    None => true,
    Some(type_condition) if type_condition == introspection_type_name => true,
    Some(type_condition) if object.implements.contains(type_condition) => true,
    Some(type_condition) => {
        schema.0.env.registry
            .types
            .get(type_condition)
            .is_some_and(|ty| ty.is_possible_type(&introspection_type_name))
    }
};

And similarly in src/resolver_utils/container.rs:

let applies_concrete_object = type_condition.is_some_and(|condition| {
    introspection_type_name == condition
        || ctx.schema_env.registry.implements
            .get(&*introspection_type_name)
            .is_some_and(|interfaces| interfaces.contains(condition))
        || ctx.schema_env.registry.types
            .get(condition)
            .is_some_and(|ty| ty.is_possible_type(&introspection_type_name))
});

Spec reference

GraphQL spec §6.3.2 — Field Collection:

If fragmentType is an Object type, the fragment spreads when objectType and fragmentType are the same. If fragmentType is an Interface or Union type, the fragment spreads when objectType is a possible type of fragmentType.

Discovered by

graphql-conformance — a project that tests open-source GraphQL implementations against a reference (graphql-js) using generated schemas and queries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment