toedter / spring-hateoas-jsonapi

A JSON:API media type implementation for Spring HATEOAS
Apache License 2.0
106 stars 15 forks source link

Regular relationship is not being deserialized successfully #56

Closed ros3cin closed 2 years ago

ros3cin commented 2 years ago

Hi! I have two simple models.

public class Increase {
  @JsonApiId
  private String id;
  @JsonApiRelationships("subject")
  private Product subject;
  private int increment;
  public Increase() {}
  public Increase(String id, Product subject, int increment) {
    this.id = id;
    this.subject = subject;
    this.increment = increment;
  }
  public Increase(Product subject, int increment) {
    this.subject = subject;
    this.increment = increment;
  }

  public void setSubject(Product subject) {
    this.subject = subject;
  }

  public Product getSubject() {
    return this.subject;
  }

  public void setId(String id) {
    this.id = id;
  }

  public String getId() {
    return this.id;
  }
  public void setIncrement(int increment) {
    this.increment = increment;
  }
  public int getIncrement() {
    return this.increment;
  }
}

@Entity
public class Product {
  @Id
  @JsonApiId
  private Long id;
  private String name;
  private String imgURL;
  private int quantity;

  public Product() {}

  public Product(Long id, String name, String imgURL, int quantity) {
    this.id = id;
    this.name = name;
    this.imgURL = imgURL;
    this.quantity = quantity;
  }
  public void setId(Long id) {
    this.id = id;
  }

  public Long getId() {
    return this.id;
  }

  public int getQuantity() {
    return quantity;
  }

  public void setQuantity(int quantity) {
    this.quantity = quantity;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getImgURL() {
    return imgURL;
  }

  public void setImgURL(String imgURL) {
    this.imgURL = imgURL;
  }
}

The Increase model is not persisted, it only represents an action. The other model, on the other hand, is persisted.

What I'm trying to do it sending a POST with the Increase data, having it deserialized so I can access the id of the Product and proceed with further code. But I'm getting this error.

java.lang.IllegalArgumentException: Can not set com.newsix.stockcontrol.Product.Product field com.newsix.stockcontrol.ChangeQuantity.Increase.subject to com.newsix.stockcontrol.Product.Product
    java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
    java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
    java.base/jdk.internal.reflect.UnsafeObjectFieldAccessorImpl.set(UnsafeObjectFieldAccessorImpl.java:81)
    java.base/java.lang.reflect.Field.set(Field.java:799)
    com.toedter.spring.hateoas.jsonapi.JsonApiEntityModelDeserializer.convertToRepresentationModel(JsonApiEntityModelDeserializer.java:132)
    com.toedter.spring.hateoas.jsonapi.JsonApiEntityModelDeserializer.convertToRepresentationModel(JsonApiEntityModelDeserializer.java:35)
    com.toedter.spring.hateoas.jsonapi.AbstractJsonApiModelDeserializer.deserialize(AbstractJsonApiModelDeserializer.java:81)
    com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
    com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3682)
    org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:380)
    org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:343)
    org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:185)
    org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:160)
    org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:133)
    org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
    org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
    org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
    org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
    org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
    org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
    org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    com.newsix.stockcontrol.Security.CredentialFilter.doFilterInternal(CredentialFilter.java:40)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    com.newsix.stockcontrol.Security.CorsFilter.doFilterInternal(CorsFilter.java:19)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)

As far as I could investigate, for some weird reason, the created Product instance (on the JsonApiEntityModelDeserializer.java line 125) is somewhat different from the inferred generic type. Some debug output below, comparing both objects.

> field.getType()
Class (Product)@13902 "class com.newsix.stockcontrol.Product.Product"
annotationData:Class$AnnotationData@13964
annotationType:null
cachedConstructor:null
classData:null
classLoader:RestartClassLoader@13965
classRedefinedCount:0
classValueMap:null
componentType:null
enumConstantDirectory:null
enumConstants:null
genericInfo:ClassRepository@13137
module:Module@13966 "unnamed module @6362e935"
name:"com.newsix.stockcontrol.Product.Product"
packageName:"com.newsix.stockcontrol.Product"
reflectionData:SoftReference@13967

> newInstance.getClass()
Class (Product)@12947 "class com.newsix.stockcontrol.Product.Product"
annotationData:null
annotationType:null
cachedConstructor:null
classData:null
classLoader:ClassLoaders$AppClassLoader@12384
classRedefinedCount:0
classValueMap:null
componentType:null
enumConstantDirectory:null
enumConstants:null
genericInfo:null
module:Module@13200 "unnamed module @1ca7bef1"
name:"com.newsix.stockcontrol.Product.Product"
packageName:"com.newsix.stockcontrol.Product"
reflectionData:SoftReference@13201

> field.getType().isAssignableFrom(newInstance.getClass())
false
toedter commented 2 years ago

Thanks for reporting this. I have tests for this deserialization use case and they are all green, so I am trying to figure out what's different in your example. You were right with the reason for this exception, the assignability check in class UnsafeObjectFieldAccessorImpl, line 81, fails. I will try to figure out why.

Probably it might have something to do that the new instance uses the AppClassLoader while the field.getType() uses the RestartClassLoader.

Could you please provide the JSON you want to deserialize, or even better, a failing test? What is your environment (Java Version, Spring Boot version, Spring HATEOAS version)?

ros3cin commented 2 years ago

O.N T.H.E S.P.O.T, @toedter !

As I saw it was a class loader related to the dev-tools, I removed the dependency and tried again, and it worked!

I'm in a hurry now, but I'll give you the info you asked (tools versions and a failing test) ASAP. By the way, by failing test, you mean a zipped example where the problem happens?

Thanks!

ros3cin commented 2 years ago

Java: 17 Spring Boot: 2.6.4 Spring HATEOAS: 1.4.1 HATEOAS JSON API: 1.3.0

payload

{
  "data": {
    "attributes": {
      "increment": 1
    },
    "relationships": {
      "subject": {
        "data": {
          "type": "products",
          "id": "2"
        }
      }
    },
    "type": "increases"
  }
}
toedter commented 2 years ago

Thanks, I try to recreate that behavior using dev-tools => no success so far. A zipped example project that demonstrates this behavior would also help :)

toedter commented 2 years ago

Since I cannot recreate this behaviour locally, I close this issue. Please re-open if necessary.