Skip to content
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

@JsonDeserialize(builder = ...) does not work in GraalVM native image #29646

Closed
dilipdhankecha2530 opened this issue Dec 6, 2022 · 11 comments
Closed
Assignees
Labels
theme: aot An issue related to Ahead-of-time processing type: enhancement A general enhancement
Milestone

Comments

@dilipdhankecha2530
Copy link

dilipdhankecha2530 commented Dec 6, 2022

Overview

We update our spring boot version to 3.0.0 and build native image with below mentioned grallvm version.

openjdk version "17.0.5" 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

While call the API which use Jackson-databind annotation then it will return with an error.

Related Issues

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 6, 2022
@mhalbritter
Copy link
Contributor

If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.

@mhalbritter mhalbritter added the status: waiting-for-feedback We need additional information before we can continue label Dec 6, 2022
@dilipdhankecha2530
Copy link
Author

@mhalbritter
Yeah, sure I will do it. and share it over here.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 6, 2022
@bclozel bclozel added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Dec 6, 2022
@dilipdhankecha2530
Copy link
Author

dilipdhankecha2530 commented Dec 6, 2022

@mhalbritter

Please, you can take a look into test.zip.

I got an error as I mentioned below.

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Builder class `com.demo.wrapper.model.Test$Builder` does not have build method (name: 'build')
 at [Source: (String)"{"id":"default"}"; line: 1, column: 1]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1909) ~[wrapper:2.14.1]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder.buildBuilderBased(BeanDeserializerBuilder.java:440) ~[na:na]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBuilderBasedDeserializer(BeanDeserializerFactory.java:362) ~[na:na]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBuilderBasedDeserializer(BeanDeserializerFactory.java:170) ~[na:na]
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:343) ~[na:na]
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264) ~[na:na]
	at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244) ~[na:na]
	at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142) ~[na:na]
	at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:648) ~[wrapper:2.14.1]
	at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4861) ~[wrapper:2.14.1]
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4731) ~[wrapper:2.14.1]
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3677) ~[wrapper:2.14.1]
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3645) ~[wrapper:2.14.1]
	at com.demo.wrapper.web.TestResource.test(TestResource.java:23) ~[wrapper:na]
	at java.base@17.0.5/java.lang.reflect.Method.invoke(Method.java:568) ~[wrapper:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[wrapper:6.0.2]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003) ~[wrapper:6.0.2]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:906) ~[wrapper:6.0.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731) ~[wrapper:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:880) ~[wrapper:6.0.2]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814) ~[wrapper:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[wrapper:10.1.1]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[wrapper:6.0.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[wrapper:6.0.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109) ~[wrapper:6.0.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[wrapper:6.0.2]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[wrapper:6.0.2]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[na:na]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[na:na]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[na:na]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[wrapper:10.1.1]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[na:na]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[wrapper:10.1.1]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[na:na]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[na:na]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400) ~[na:na]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[wrapper:10.1.1]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861) ~[na:na]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739) ~[na:na]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[wrapper:10.1.1]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[na:na]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[na:na]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[na:na]
	at java.base@17.0.5/java.lang.Thread.run(Thread.java:833) ~[wrapper:na]
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775) ~[wrapper:na]
	at org.graalvm.nativeimage.builder/com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203) ~[na:na]

You can use below curl to reproduce the issue.

curl --location --request POST 'localhost:8080/api/test' \
--header 'Content-Type: text/plain' \
--data-raw '{"id":"default"}'

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 6, 2022
@mhalbritter mhalbritter changed the title com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Builder class com.demo.Test$Builder does not have build method (name: 'build') @JsonDeserialize(builder = ...) doesn't work in native-image Dec 6, 2022
@mhalbritter
Copy link
Contributor

mhalbritter commented Dec 6, 2022

Thanks for the sample. It looks like that the Jackson builder feature is not supported yet.

@JsonDeserialize(builder = Test.Builder.class)

You can either remove the builder or register the ctor and methods from your builder to be reflection-enabled:

    class TestresourceRuntimeHints implements RuntimeHintsRegistrar {
        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.reflection().registerType(Test.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
        }
    }

and then use

@ImportRuntimeHints(TestresourceRuntimeHints.class)

on any bean.

@bclozel bclozel transferred this issue from spring-projects/spring-boot Dec 6, 2022
@sbrannen sbrannen changed the title @JsonDeserialize(builder = ...) doesn't work in native-image @JsonDeserialize(builder = ...) does not work in GraalVM native image Dec 6, 2022
@sbrannen
Copy link
Member

sbrannen commented Dec 6, 2022

If we address this issue, I think we should reopen #29386 and address it as well.

@sbrannen sbrannen added type: enhancement A general enhancement theme: aot An issue related to Ahead-of-time processing labels Dec 6, 2022
@sbrannen sbrannen added this to the Triage Queue milestone Dec 6, 2022
@bclozel
Copy link
Member

bclozel commented Dec 6, 2022

As part of this issue, we should consider whether we want to introduce specific AOT rules for Jackson. Since there are many features and variants in Jackson, I'm not sure we should invest in that area.

@bclozel bclozel removed this from the Triage Queue milestone Jan 17, 2023
@bclozel
Copy link
Member

bclozel commented Jan 17, 2023

We could expand the scope of our @RegisterReflectionForBinding and use it to process method parameters and return types of controller methods.

This means supporting various features like:

  • @JsonNaming strategies
  • @JsonDeserialize, @JsonSerialize
  • @JsonGetter, @JsonSetter @JsonValue
  • @JsonCreator
  • Detecting @JsonSubTypes types if only the interface type is detected through AOT processing

We could also refine the binding algorithm to include/ignore @JsonIgnoreProperties, @JsonIgnore and @JsonIgnoreType, @JsonAutoDetect annotated elements.

All of that should also support custom annotations with @JacksonAnnotationsInside.
Note that all of those features can be turned off by a Jackson configuration flag through the MapperFeature, which is not accessible during the AOT phase.

With that in mind, I'm not in favor of implementing Jackson-specific behavior, but keep using our general "reflection for binding" that matches a lot of cases already. I think this should be handled in a GraalVM Feature or better, through a build-time mechanism provided by the Jackson project itself: keeping up with all the changes in Jackson is an important maintenance burden in this space.

@mhalbritter
Copy link
Contributor

keeping up with all the changes in Jackson is an important maintenance burden in this space.

I agree on not implementing Jackson-specific behavior.

@sdeleuze
Copy link
Contributor

On one side I understand our reluctance related to the maintenance burden especially given the various annotations we would have to support, on another side:

  • We need to be consistent. Why would we support JPA @Converter and @Convert annotation like we do in PersistenceManagedTypesBeanRegistrationAotProcessor and not Jackson similar use case?
  • Spring is already in charge of providing hints for classes serialized by Jackson via the knowledge it has of related classes via for example Spring MVC/WebFlux programming model or RegisterReflectionForBinding, conceptually this is similar.
  • The end user will have to go threw the pain of multiple native build failure which is not ideal
  • Jackson is probably the most popular dependency we support

Since BindingReflectionHintsRegistrar already has some logic related to the handing of @JacksonAnnotation, my proposal would be simply to refine that logic to add invoke hints on declared constructors for class attribute of annotations annotated with @JacksonAnnotation. Looks like a small change, rather maintainable which translate as a big improvement for native users. Any thoughts?

@sbrannen
Copy link
Member

Since BindingReflectionHintsRegistrar already has some logic related to the handing of @JacksonAnnotation, my proposal would be simply to refine that logic to add invoke hints on declared constructors for class attribute of annotations annotated with @JacksonAnnotation. Looks like a small change, rather maintainable which translate as a big improvement for native users. Any thoughts?

I agree that not doing anything additional for Jackson use cases will put a large burden on our users. If the Jackson team some day introduces a GraalVM Feature (or similar), that would of course be great, but who knows if that day will ever come.

So, if we can make a big improvement with such a small change like you've proposed Sebastien, I am in favor of that.

@sdeleuze sdeleuze self-assigned this Jan 20, 2023
@sdeleuze sdeleuze removed for: team-attention status: waiting-for-triage An issue we've not yet triaged or decided on labels Jan 20, 2023
@sdeleuze sdeleuze removed the status: feedback-provided Feedback has been provided label Jan 20, 2023
@sdeleuze sdeleuze added this to the 6.0.5 milestone Jan 20, 2023
@sdeleuze
Copy link
Contributor

After discussing with @bclozel and @jhoeller, let's give it a try. If the implementation is more involved than expected, we may reconsider implementing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: aot An issue related to Ahead-of-time processing type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

6 participants