spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
919 stars 563 forks source link

Persistent Property of type Map throws UnrecognizedPropertyException [DATAREST-178] #563

Open spring-projects-issues opened 11 years ago

spring-projects-issues commented 11 years ago

Petar Tahchiev opened DATAREST-178 and commented

Hi guys,

I have the following JPA mapping:

@Entity
public class Product {

    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "product_name_lv", joinColumns = @JoinColumn(name = "product_pk"))
    @MapKeyColumn(name = "locale")
    @MapKeyJoinColumn(name = "language", referencedColumnName = "isocode")
    private Map<Locale, LocalizedValue> name = new HashMap<Locale, LocalizedValue>();

}

where the LocalizedValue is a simple POJO class:

@Embeddable
public class LocalizedValue {
    @Column(name = "VALUE")
    private String value;
}

So SDR works good and returns my product, like a normal json:

"name" : {
  "bg" : {
    "value" : "FOO"
  },
  "en" : {
    "value" : "BAR"
  },
  "de" : {
    "value" : "German FOO"
  }
},

So far so good I try to make a PUT request with the same json data, it yields with:

2013-10-19 13:05:57,726 [qtp508751118-134] ERROR: Could not read JSON: Unrecognized field "en" (class com.xxxxxx.platform.core.model.i18n.LocalizedValue), not marked as ignorable (one known property: "value"])
 at [Source: org.eclipse.jetty.server.HttpInput@63497f5b; line: 1, column: 30] (through reference chain: com.xxxxx.platform.core.model.i18n.LocalizedValue["en"]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "en" (class com.xxxxxx.platform.core.model.i18n.LocalizedValue), not marked as ignorable (one known property: "value"])
 at [Source: org.eclipse.jetty.server.HttpInput@63497f5b; line: 1, column: 30] (through reference chain: com.xxxxx.platform.core.model.i18n.LocalizedValue["en"])
org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Unrecognized field "en" (class com.xxxxxx.platform.core.model.i18n.LocalizedValue), not marked as ignorable (one known property: "value"])
 at [Source: org.eclipse.jetty.server.HttpInput@63497f5b; line: 1, column: 30] (through reference chain: com.xxxxxx.platform.core.model.i18n.LocalizedValue["en"]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "en" (class com.xxxxxx.platform.core.model.i18n.LocalizedValue), not marked as ignorable (one known property: "value"])
 at [Source: org.eclipse.jetty.server.HttpInput@63497f5b; line: 1, column: 30] (through reference chain: com.xxxxxx.platform.core.model.i18n.LocalizedValue["en"])
    at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readJavaType(MappingJackson2HttpMessageConverter.java:181)
    at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readInternal(MappingJackson2HttpMessageConverter.java:166)
    at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:153)
    at org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:48)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:77)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:162)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:123)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:745)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:686)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:936)
    at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:849)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:758)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:812)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:686)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1494)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
    at org.apache.logging.log4j.core.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:66)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1482)
    at com.xxxxxx.platform.modules.rest.mvc.filter.CorsFilter.doFilterInternal(CorsFilter.java:23)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1474)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:499)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:137)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:557)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:231)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1086)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:428)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:193)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1020)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:135)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:255)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:154)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:116)
    at org.eclipse.jetty.server.Server.handle(Server.java:370)
    at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:489)
    at org.eclipse.jetty.server.AbstractHttpConnection.content(AbstractHttpConnection.java:960)
    at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.content(AbstractHttpConnection.java:1021)
    at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:865)
    at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:240)
    at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:82)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:668)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:52)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:608)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:543)
    at java.lang.Thread.run(Thread.java:722)

So I started digging in the code and I think the problem happens in PersistentEntityJackson2Module:197. I can see there an if-statement which checks if the property is of type map:

} else if (persistentProperty.isMap()) {
    Class<? extends Map<?, ?>> mtype = (Class<? extends Map<?, ?>>) persistentProperty.getType();
    Map<Object, Object> m = (Map<Object, Object>) wrapper.getProperty(persistentProperty);
    if (null == m || m == Collections.EMPTY_MAP) {
        m = new HashMap<Object, Object>();
    }

    if ((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
        do {
            name = jp.getCurrentName();
            // TODO resolve domain object from URI
            tok = jp.nextToken();
            Object mval = jp.readValueAs(persistentProperty.getMapValueType()); //<-- this line reads as persistent property map value type.

            m.put(name, mval);
        } while ((tok = jp.nextToken()) != JsonToken.END_OBJECT);

        val = m;
    } else if (tok == JsonToken.VALUE_NULL) {
        val = null;
    } else {
        throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map.");
    }

So it basically gets the type of the persistent property, creates an empty map, iterates over the json, and reads the value of the original map (i.e. what will be placed after name: ) as the value of the persistent property map (LocalizedValue in my case). Which is wrong I think. It should read it as a map imho


Affects: 2.0 M1 (Codd)

1 votes, 3 watchers

spring-projects-issues commented 10 years ago

Petar Tahchiev commented

Can anyone fix this (if it's a bug).. it's blocking me now - I cannot save my entities or create new ones :(

spring-projects-issues commented 10 years ago

Oliver Drotbohm commented

Would you mind trying this with the latest release or even the snapshots for upcoming 2.0.1 and 2.1? The code you seem to have spotted the error in has undergone quite a rewrite prior to 2.0 release so we might just have fixed this while doing so

spring-projects-issues commented 10 years ago

Petar Tahchiev commented

Hi Oliver, thanks for your reply. Indeed it's a different error now. Now what I get is:

2014-03-12 15:25:40,269 [qtp97186485-163] WARN : Failed to evaluate deserialization for type [simple type, class com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel]: com.fasterxml.jackson.databind.JsonMappingException: Conflicting setter definitions for property "name": ccom.xxxxx.yyyyy.zzzz.model.catalog.ProductModel#setName(1 params) vs com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel#setName(1 params)
2014-03-12 15:25:40,274 [qtp97186485-163] WARN : Failed to evaluate deserialization for type [simple type, class com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel]: com.fasterxml.jackson.databind.JsonMappingException: Conflicting setter definitions for property "name": com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel#setName(1 params) vs com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel#setName(1 params)
[ERROR] No suitable HttpMessageConverter found to read request body into object of type class com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel from request with content type of application/json;charset=UTF-8!
org.springframework.http.converter.HttpMessageNotReadableException: No suitable HttpMessageConverter found to read request body into object of type class com.xxxxx.yyyyy.zzzz.model.catalog.ProductModel from request with content type of application/json;charset=UTF-8!
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:109)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:79)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:157)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:124)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:690)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:874)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:717)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1644)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:108)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1632)
    at com.xxxxx.yyyyy.zzzz.storefront.filter.CorsFilter.doFilterInternal(CorsFilter.java:34)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:108)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1632)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:108)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1624)
    at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:164)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1615)
    at org.apache.logging.log4j.core.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:66)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1615)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:550)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:568)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:221)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1110)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:479)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:183)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1044)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:199)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:109)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:459)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:281)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:232)
    at org.eclipse.jetty.io.AbstractConnection$1.run(AbstractConnection.java:505)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:607)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:536)
    at java.lang.Thread.run(Thread.java:722)

And yes, in ProductModel I have 2 methods called setName, the one accepts a Map<Locale, LocalizedValue> and the other one a single Map.Entry<Locale, LocalizedValue>. Is this limitation of Spring or Jackson? How can I overcome it?

spring-projects-issues commented 10 years ago

Oliver Drotbohm commented

That's a Jackson thing. AFAIR you need to ignore either one of them using @JsonIgnore

spring-projects-issues commented 10 years ago

Petar Tahchiev commented

The problem is that if I set the @JsonIgnore at only one of the setter methods the property doesn't get exported at all.. I guess I will have to ditch one of the methods :( ... You can go on and close the issue