Netflix / dgs-framework

GraphQL for Java with Spring Boot made easy.
https://netflix.github.io/dgs
Apache License 2.0
3.06k stars 295 forks source link

bug: Using UUID as ID! inside input results in Cannot cast java.lang.String to java.util.UUID #1508

Closed vonElfvin closed 1 year ago

vonElfvin commented 1 year ago

Setup: I have a quite fresh Spring Boot 3 application using Java 19 and DGS 6.0.5.

Expected behavior

Able to have a UUID in the Kotlin code and a ID! in the schema.graphql. I'm pretty sure this used to work when I used graphql-java so I'm guessing it's a bug and not a feature request.

schema.graphls

extend type Mutation {
    organizationDoTheThing(input: OrganizationDoTheThingInput!): OrganizationThingDone!
}

input OrganizationDoTheThingInput {
  organizationId: ID! # This is the thing that fails
}

OrganizationResolver.kt

class OrganizationDoTheThingInput(
    val organizationId: UUID // This is the thing that fails
)

@DgsMutation
fun organizationDoTheThing(
    @InputArgument input: OrganizationDoTheThingInput,
): OrganizationThingDone {
    // Not interesting
}

Actual behavior

Getting an exception:

(Log: 2023-05-01T18:01:51.034+02:00 WARN 28187 --- [o-11337-exec-10] n.g.e.SimpleDataFetcherExceptionHandler : Exception while fetching data (/organizationDoTheThing) : Failed to convert from type [java.util.LinkedHashMap<?, ?>] to type [@com.netflix.graphql.dgs.InputArgument com.server.domains.organization.OrganizationResolver$OrganizationDoTheThingInput] for value '{organizationId=bf3d722a-d186-4478-add8-25cbe56c4d8b}' )

org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.LinkedHashMap<?, ?>] to type [@com.netflix.graphql.dgs.InputArgument com.granat.linker.domains.users.organization.OrganizationResolver$OrganizationGenerateTokenInput] for value '{organizationId=d34de611-7ebf-4d6d-9305-6a054b42c5c6}'
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
    at com.netflix.graphql.dgs.internal.method.AbstractInputArgumentResolver.convertValue(AbstractInputArgumentResolver.kt:93)
    at com.netflix.graphql.dgs.internal.method.AbstractInputArgumentResolver.resolveArgument(AbstractInputArgumentResolver.kt:49)
    at com.netflix.graphql.dgs.internal.method.ArgumentResolverComposite.resolveArgument(ArgumentResolverComposite.kt:39)
    at com.netflix.graphql.dgs.internal.DataFetcherInvoker.invokeKotlinMethod(DataFetcherInvoker.kt:93)
    at com.netflix.graphql.dgs.internal.DataFetcherInvoker.get(DataFetcherInvoker.kt:63)
    at graphql.schema.DataFetcherFactories.lambda$wrapDataFetcher$2(DataFetcherFactories.java:37)
    at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:86)
    at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:282)
    at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:211)
    at graphql.execution.ExecutionStrategy.resolveField(ExecutionStrategy.java:183)
    at graphql.execution.AsyncSerialExecutionStrategy.lambda$execute$1(AsyncSerialExecutionStrategy.java:47)
    at graphql.execution.Async.eachSequentiallyImpl(Async.java:191)
    at graphql.execution.Async.eachSequentially(Async.java:180)
    at graphql.execution.AsyncSerialExecutionStrategy.execute(AsyncSerialExecutionStrategy.java:42)
    at graphql.execution.Execution.executeOperation(Execution.java:159)
    at graphql.execution.Execution.execute(Execution.java:105)
    at graphql.GraphQL.execute(GraphQL.java:645)
    at graphql.GraphQL.lambda$parseValidateAndExecute$11(GraphQL.java:564)
    at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
    at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2341)
    at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:559)
    at graphql.GraphQL.executeAsync(GraphQL.java:527)
    at com.netflix.graphql.dgs.internal.BaseDgsQueryExecutor.baseExecute(BaseDgsQueryExecutor.kt:127)
    at com.netflix.graphql.dgs.internal.DefaultDgsQueryExecutor.execute(DefaultDgsQueryExecutor.kt:83)
    at com.netflix.graphql.dgs.mvc.DgsRestController$graphql$executionResult$1.invoke(DgsRestController.kt:206)
    at com.netflix.graphql.dgs.mvc.DgsRestController$graphql$executionResult$1.invoke(DgsRestController.kt:204)
    at com.netflix.graphql.dgs.internal.utils.TimeTracer.logTime(TimeTracer.kt:24)
    at com.netflix.graphql.dgs.mvc.DgsRestController.graphql(DgsRestController.kt:204)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:563)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at com.granat.linker.domains.auth.lib.AuthenticationFilter.doFilterInternal$proceed(SecurityConfig.kt:54)
    at com.granat.linker.domains.auth.lib.AuthenticationFilter.doFilterInternal(SecurityConfig.kt:91)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
    at ch.qos.logback.access.tomcat.LogbackValve.invoke(LogbackValve.java:267)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:1589)
Caused by: com.netflix.graphql.dgs.exceptions.DgsInvalidInputArgumentException: Provided input arguments do not match arguments of data class `class com.granat.linker.domains.users.organization.OrganizationResolver$OrganizationGenerateTokenInput`
    at com.netflix.graphql.dgs.internal.DefaultInputObjectMapper.mapToKotlinObject(DefaultInputObjectMapper.kt:103)
    at com.netflix.graphql.dgs.internal.method.InputObjectMapperConverter.convert(InputObjectMapperConverter.kt:38)
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    ... 87 common frames omitted
Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:70)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
    at kotlin.reflect.jvm.internal.calls.CallerImpl$Constructor.call(CallerImpl.kt:41)
    at kotlin.reflect.jvm.internal.KCallableImpl.callDefaultMethod$kotlin_reflection(KCallableImpl.kt:188)
    at kotlin.reflect.jvm.internal.KCallableImpl.callBy(KCallableImpl.kt:111)
    at com.netflix.graphql.dgs.internal.DefaultInputObjectMapper.mapToKotlinObject(DefaultInputObjectMapper.kt:101)
    ... 89 common frames omitted
Caused by: java.lang.ClassCastException: Cannot cast java.lang.String to java.util.UUID
    at java.base/java.lang.Class.cast(Class.java:3946)
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67)
    ... 95 common frames omitted

Looking inside the DefaultInputObjectMapper these lines call the constructor without taking into account the UUID being a String:

 } else { // This is the statement that is hit, input is a string containing "bf3d722a-d186-4478-add8-25cbe56c4d8b"
          parametersByName[parameter] = input
      }
  }

  return try {
      constructor.callBy(parametersByName)
  } catch (ex: Exception) {
      throw DgsInvalidInputArgumentException("Provided input arguments do not match arguments of data class `$targetClass`", ex)
  }

It works when I do organizationId: String inside the Kotlin class and use UUID.fromString manually in the @DgsMutation function.

Steps to reproduce

Should be reproduceable by following setup above. Unless something else is off with my setup.

Note: A test case would be highly appreciated, but we understand that's not always possible

srinivasankavitha commented 1 year ago

ID is a standard scalar that is serialized to a String by graphql-java (https://www.graphql-java.com/documentation/data-mapping#scalars), and cannot be overridden (https://github.com/graphql-java/graphql-java/issues/1420) If you would like to use UUID, you will need to use it as a custom scalar. The graphql-java-extended-scalars library offers a scalar implementation for UUID that you can use as well: https://github.com/graphql-java/graphql-java-extended-scalars#id-scalars.

vonElfvin commented 1 year ago

Oh, you're right! Looking at the actual schema.graphqls code of my old graphl-java usage I had exactly that instead of an ID! 🤦 I guess my brain had planted a pseudo-memory that it used to work for inputs as well since it works for type.

Thanks!