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

Deserializer customizations not propagated to Jackson creator properties [DATAREST-1393] #1752

Closed
spring-projects-issues opened this issue Jun 12, 2019 · 10 comments
Assignees
Labels

Comments

@spring-projects-issues
Copy link

@spring-projects-issues spring-projects-issues commented Jun 12, 2019

Florent Biville opened DATAREST-1393 and commented

I have a simple Kotlin project, generated from start.spring.io, Spring Boot version 2.1.5.RELEASE.

Two entities:

@Entity
class Venue(@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 var id: Long? = null,
 var name: String,
 var address: String)

and:

@Entity
class Session(@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 var id: Long? = null,
 var title: String,
 @Temporal(TemporalType.TIMESTAMP)
 var date: Date,
 @ManyToOne var venue: Venue)

As well as two basic exported repositories: 

interface VenueRepository : JpaRepository<Venue, Long> 

and:

@RepositoryRestResource(excerptProjection = SessionWithVenue::class)
interface SessionRepository : JpaRepository<Session, Long>

I can save a venue without any problem.

 

However, when I try to save a new session with an existing venue, I get the following error:

2019-06-12 11:38:39.652 ERROR 29458 — [nio-8080-exec-2] o.s.d.r.w.RepositoryRestExceptionHandler : JSON parse error: Cannot construct instance of `net.hackergarten.sessionapi.venue.Venue` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/venues/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `net.hackergarten.sessionapi.venue.Venue` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/venues/1')
 ` at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 68] (through reference chain: net.hackergarten.sessionapi.session.Session["venue"])`org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `net.hackergarten.sessionapi.venue.Venue` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/venues/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `net.hackergarten.sessionapi.venue.Venue` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/venues/1')
 ` at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 68] (through reference chain: net.hackergarten.sessionapi.session.Session["venue"])`
 ` at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:245) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:219) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:199) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.read(PersistentEntityResourceHandlerMethodArgumentResolver.java:230) ~[spring-data-rest-webmvc-3.1.8.RELEASE.jar:3.1.8.RELEASE]`
 ` at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.lambda$read$7(PersistentEntityResourceHandlerMethodArgumentResolver.java:188) ~[spring-data-rest-webmvc-3.1.8.RELEASE.jar:3.1.8.RELEASE]`
 ` at java.base/java.util.Optional.orElseGet(Optional.java:369) ~[na:na]`
 ` at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.read(PersistentEntityResourceHandlerMethodArgumentResolver.java:188) ~[spring-data-rest-webmvc-3.1.8.RELEASE.jar:3.1.8.RELEASE]`
 ` at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:133) ~[spring-data-rest-webmvc-3.1.8.RELEASE.jar:3.1.8.RELEASE]`
 ` at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at javax.servlet.http.HttpServlet.service(HttpServlet.java:660) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) ~[spring-webmvc-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:836) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1747) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]`
 ` at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]`
 ` at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.19.jar:9.0.19]`
 ` at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]`
 Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `net.hackergarten.sessionapi.venue.Venue` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/venues/1')
 ` at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 68] (through reference chain: net.hackergarten.sessionapi.session.Session["venue"])`
 ` at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1343) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1032) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1373) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:171) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:530) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:528) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:417) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1287) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084) ~[jackson-databind-2.9.8.jar:2.9.8]`
 ` at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:239) ~[spring-web-5.1.7.RELEASE.jar:5.1.7.RELEASE]`
 ` ... 58 common frames omitted`

The workarounds consist in rewriting the entities as follows:

@Entity
class Session() {

 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 var id: Long? = null var title: String = "" @Temporal(TemporalType.TIMESTAMP)
 var date: Date = Date(0)
 @ManyToOne var venue: Venue = Venue()

}
@Entity
class Venue() {

 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 var id: Long? = null var name: String = "" var address: String = ""
}

The no-arg constructor generated by the JPA Kotlin compiler plugin should be enough. What does one need to write a no-arg constructor explicitly?

If you want to reproduce the issue, you can follow these steps:

$> git clone https://github.com/Agatesse/hackergarten-session.git -b jackson_bug
$> cd jackson_bug
$> mvn spring-boot:run

In another terminal: 

$> venue_json="{\"name\": \"Pivotal France\", \"address\": \"33 rue La Fayette 75009 Paris\"}"
$> venue_uri=$(curl --request POST --header "Content-Type:application/json" --data "${venue_json}" http://localhost:8080/api/venues | jq --raw-output '._links.self.href')

$> now=$(date +%s000)
$> session_json="{\"title\": \"Hackergarten Paris 42\", \"date\": ${now}, \"venue\": \"${venue_uri}\"}"
$> curl --request POST --header "Content-Type:application/json" --data "${session_json}" http://localhost:8080/api/sessions

 


Affects: 3.2 RC1 (Moore), 3.1.8 (Lovelace SR8)

Backported to: 3.1.10 (Lovelace SR10)

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Oliver Drotbohm commented

I suspect that the Spring Data REST Jackson bean deserialization customization is broken for some reason. Usually it configures a dedicated serializer from URIs for properties that point to aggregate roots that have repositories exposed. That doesn't seem to kick in here and I suspect some Kotlin specific type visibility issues.

Is there a chance you formulate the HTTP interactions as integration tests so that I can properly debug the issue? I couldn't get the terminal interaction to work properly as the JSON broke (probably some escaping issues not working in Bash.

Oh, and it would be helpful to know if the example works OOTB if written in Java :)

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Florent Biville commented

I fixed the escaping for Bash (worked fine in Zsh)

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Florent Biville commented

Now you can just run mvn test -Dtest="net.hackergarten.sessionapi.session.SessionApiTest#post a new session" and it will fail in the same way. (The test hardcodes a few things but it should be enough to reproduce)

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Oliver Drotbohm commented

Lovely, Florent. Thanks. I'll have a look

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Oliver Drotbohm commented

Oh, wait. What do I actually run the command on? Do you have a sample project to share?

EDIT: Forgive my ignorance, I just saw your cloning instructions 🙄😬

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Florent Biville commented

Yes, as written above: git clone https://github.com/Agatesse/hackergarten-session.git -b jackson_bug

Pushed another branch where the entity is rewritten in Java, and there is no issue at all:

$> git checkout jackson_bug_java
$> mvn test -Dtest="net.hackergarten.sessionapi.session.SessionApiTest#post a new session"

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 24, 2019

Oliver Drotbohm commented

That's helpful, too, thanks

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 25, 2019

Oliver Drotbohm commented

The situation is caused by the property customization being overridden by Jackson looking up creator properties via the registered ValueInstantiator and one creating new properties from the constructor of the owning type. I filed a ticket for the problem and have a workaround that tweaks the constructor arguments via reflection here locally. About to polish and push

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 25, 2019

Florent Biville commented

Great news, thanks!

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Jun 25, 2019

Oliver Drotbohm commented

That's fixed now. Your sample project builds successfully with the latest snapshots

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

No branches or pull requests

2 participants