Skip to content

Schema inspection support for interface and union types #924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rstoyanchev opened this issue Mar 15, 2024 · 5 comments
Closed

Schema inspection support for interface and union types #924

rstoyanchev opened this issue Mar 15, 2024 · 5 comments
Assignees
Labels
type: enhancement A general enhancement
Milestone

Comments

@rstoyanchev
Copy link
Contributor

Currently schema inspection relies on a one-for-one match between a schema field type and a controller method return type, and that doesn't extend well to interfaces and unions as the Java types used at runtime are unknown.

We can provide support for inspection of unions and interfaces with a little bit of help from application to resolve Java types. We can navigate schema interface type hierarchies and union member types, and resolve corresponding Java types with the help of a resolver that does the reverse of what TypeResolver does. A default implementation for such a resolver can look for matching Java types under a given root package. We can also use any Java to schema type registrations that are provided in ClassNameTypeResolver.

@bclozel
Copy link
Member

bclozel commented Mar 20, 2024

Rossen and I discussed this and there are several ways to achieve this.

Classpath scanning

We can consider that there is no expected "union type" formalism in the application code, meaning that unions could be expressed with marker interfaces or even plain Object return types on controller methods. This means there is no straightforward way to detect all types related to a union. We can then use a reverse TypeResolver approach, as suggested in the issue description here and run candidates types that were scanned on the application classpath.

This approach:

  • requires an additional classpath scanning during application startup, which can be a performance issue
  • would require user input to list packages to be scanned
  • would also require a default heuristic for guessing good candidates for scanning; for example, by looking at all discovered types and scanning in those packages. This would mean that the introspection in this case would require a second pass?
  • does no put any restriction on how unions are implemented in the application, so maximum flexibility for applications
  • requires specific support for GraalVM, as classpath scanning and reading metadata from class files is not an option there; most likely a code generation solution is required
  • can miss classpath locations, depending on the strategy chosen by the application: are those types generated, located in a different module or package?

Sealed interfaces

We can also consider a different approach, this time restricting how union types can be implemented in the application. We could require that apps use sealed interfaces in Java and Kotlin. Such types provide the list of allowed types through direct reflection calls (like java.lang.Class#getPermittedSubclasses).

This approach:

  • does not require any classpath scanning
  • GraalVM support would be direct
  • this puts a restriction on how union types can be implemented, if the developers want the schema inspection to detect them. Other strategies would be still supported at runtime anyway.
  • no need for heuristics and refinements, the relationship is straightforward

It seems that existing code generation tools chose sealed interfaces as a way to map union types in Java and Kotlin: both DGS codegen and Apollo GraphQL Kotlin (see apollographql/apollo-kotlin#1390 and apollographql/apollo-kotlin#2726) chose this approach for quite some time now.

rstoyanchev added a commit that referenced this issue Mar 25, 2024
rstoyanchev added a commit that referenced this issue Mar 25, 2024
Refactor logic to determine the GraphQL type and Java type for a
field into private, nested class.

See gh-924
@rstoyanchev
Copy link
Contributor Author

rstoyanchev commented Mar 25, 2024

The final solution is close to the original description, but instead of scanning under a root package, the inspection looks in the same package as the controller method return type for the interface or union. For example, if the controller method returns an interface, or a sealed interface for a union, implementations in the same package can be located if the class name matches or there is a naming convention to match GraphQL type to Java class names.

See the reference documentation for more details.

@smilyanovr it would be great if you could try this with 1.3.0-SNAPSHOT related to #739 and #744.

@smilyanovr
Copy link

@rstoyanchev sorry for the delay. It took me a while until I figure out how to update all dependencies to use the snapshot version. I did test the snapshot version and everything looks good to me. Final solution on the type resolution seems reasonable.

@rstoyanchev
Copy link
Contributor Author

Thanks for taking the time to give it a try. Was any explicit configuration necessary to resolve schema types to classes?

@smilyanovr
Copy link

I used some test projects to test the changes and no additional configuration was needed. There might be some cases where additional configuration is needed, but that will be tested later when our production projects get updated to the newer version.
Looks good to me at this point of time. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants