Skip to content

Support of Reactor's types like Mono<T> and Flux<T>? #103

Closed

Description

@sp00m

I'm building a Spring Boot 2 project with Reactive Web (Spring WebFlux), which relies on Reactor. My services are thus returning Mono<T>s and Flux<T>s.

The problem is that I can't manage to make my GraphQLSchema handle those types correctly.


For example, say I have the following schema:

type Query {
  article(
    id: Long!
  ): Article!
}

type Article {
  id: Long!
  name: String!
}

And the following QueryResolver:

public class QueryResolver implements GraphQLQueryResolver {
    
  private final ArticleService articleService;

  public QueryResolver(ArticleService articleService) {
    this.articleService = articleService;
  }

  public Mono<Article> getArticle(long id) {
    return articleService.findOneById(id);
  }
    
}

Building the schema:

GraphQLSchema schema = SchemaParser.newParser()
  .file("schema.graphqls")
  .resolvers(queryResolver)
  .build()
  .makeExecutableSchema();
GraphQL build = GraphQL.newGraphQL(schema).build();

When running a query:

ExecutionResult executionResult = build.execute("...");

I catch the following exception:

TypeClassMatcher$RawClassRequiredForGraphQLMappingException: Type reactor.core.publisher.Mono<my.pkg.Article> cannot be mapped to a GraphQL type!  Since GraphQL-Java deals with erased types at runtime, only non-parameterized classes can represent a GraphQL type.  This allows for reverse-lookup by java class in interfaces and union types.

I then tried to register a SchemaParserOptions.GenericWrapper for the Mono<T> type:

SchemaParserOptions options = SchemaParserOptions.newOptions()
  .genericWrappers(new SchemaParserOptions.GenericWrapper(Mono.class, 0))
  .build();

After having passed these options to the SchemaParser above, when running a query, I now catch the following exception:

ResolverError: Expected source object to be an instance of 'my.pkg.Article' but instead got 'reactor.core.publisher.MonoCompletionStage'

I wanted to try to register a SchemaParserOptions.GenericWrapper for MonoCompletionStage as well, but the class is only visible package-wise.

Any idea how I could proceed?


Just a naive idea of mine: couldn't inheritance be checked when the SchemaParserOptions.GenericWrappers are used? As MonoCompletionStage<T> is a subtype of Mono<T>, registering a SchemaParserOptions.GenericWrapper for Mono<T>could then take into account the subtypes of Mono<T> somehow... Note that I have no idea of the technical involvements of this ;)

In this case, because you're already registering a SchemaParserOptions.GenericWrapper for Publisher<T> if I use .useDefaultGenericWrappers(true), I even wouldn't have to register a SchemaParserOptions.GenericWrapper for Mono<T> as it extends Publisher<T>.


In the meantime, I'm converting my Mono<T> into a CompletableFuture<T>:

public CompletableFuture<Article> getArticle(long id) {
  return articleService.findOneById(id).toFuture();
}

And then converting the query result back into a Mono<T> to make it WebFlux-compliant:

Mono.fromFuture(result);

Activity

  1. apottere commented on Jan 12, 2018

    @apottere
    Collaborator

    Makes sense, I can look into checking for a subclass of a generic wrapper.

  2. sp00m commented on Feb 21, 2018

    @sp00m
    ContributorAuthor

    Little update: I've started playing with @Batched fetchers to avoid "select n+1", it would be pretty elegant if I could simply return a Flux from these! For example, say one Article is linked to one Blog:

    public class ArticleResolver implements GraphQLResolver<Article> {
        
      private final BlogService blogService;
    
      public ArticleResolver(ArticleService articleService) {
        this.articleService = articleService;
      }
    
      @Batched
      public Flux<Blog> getBlog(List<Article> articles) {
        return blogService.findByArticles(articles);
      }
        
    }

    In the above example, blogService.findByArticles(articles) returns a Flux<Blog> (pushing as many Blogs as inputted Articles, plus making sure the inputted Articles order matches the the one Blogs have been pushed into the Flux... quite a lot of conditions, but this isn't graphql-java-tools's matter anyway).


    A less-but-still elegant API would be to use a Mono, e.g.:

    @Batched
    public Mono<List<Blog>> getBlog(List<Article> articles) {
      // ...
    }

    What do you think? I'm afraid all this is going kind of out of scope for graphql-java-tools though, making it quite tightly coupled with Reactor...

  3. sp00m commented on Feb 21, 2018

    @sp00m
    ContributorAuthor

    Wait, thinking of my original issue again, there's something I don't fully understand: registering a GenericWrapper for Mono actually made the engine successfully validate the schema when creating the GraphQLSchema instance. The issue I then faced was at query execution time, because I guess the engine simply doesn't know how to get the actual wrapped value out of the Mono (more specifically out of the MonoCompletionStage, subtype of Mono), which is kind of expected as I haven't registered any kind of converter, am I wrong?

    If this is true, then what's the use case of actually registering custom GenericWrappers? Because the engine won't ever know how to get the actual value out of it, as it's indeed custom... What did I misunderstand here?

  4. sp00m commented on Feb 21, 2018

    @sp00m
    ContributorAuthor

    Actually, maybe there something to do around MethodFieldResolverDataFetcher#get(...)?

    Because, if I understand correctly, this line basically unwraps an Optional (if the returned value is an Optional obviously), which is actually what could be done for a Mono (i.e. converting it into a Future thanks to Mono#toFuture(), as the engine already knows how to handle Futures).

    So maybe, instead of having this hardcoded only for Optional, we could think of a way for integrating apps to register custom converters, just like you can currently do for GenericWrappers? Then, this Optional conversion could be registered by default, among the possible custom ones.

    @apottere, what do you think of all this?

    Or a possibly even better approach could be to make these GenericWrappers optionally aware of how to convert from their wrapping type to their wrapped type. Optional could then be a GenericWrapper directly, which would then not only make the above conversion more dynamic, but also the schema check.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions