Skip to content

POST/PUT to entity with @JsonProperty annotated relation does not work #2165

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

Closed
vierbergenlars opened this issue Jul 27, 2022 · 2 comments
Closed
Assignees
Labels
type: bug A general bug

Comments

@vierbergenlars
Copy link
Contributor

vierbergenlars commented Jul 27, 2022

Minimal reproduction example

Given a simple project with a JPA entity having a relation to an other JPA entity, both with exported repositories.

Normally, when creating and updating (POST/PUT) an entity via the REST endpoints, you can use the URL of the relation target.
(e.g.: send {"package": "/packages/1"} when package is a JPA @OneToOne relation)

However, this does not work when the JPA relation is annotated with @JsonProperty() to change the serialized name.

JPA entity setup
package eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.rest.core.annotation.RestResource;

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;

    @OneToOne(optional = false)
    @JsonProperty("package") // <--- This is the property that was renamed, because package is a reserved keyword, we can't use it as a field name.
    @RestResource(rel = "package", path = "package")
    private Package _package;
}

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Package {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;
}
When we retrieve the JSON-schema for `Product`, it specifies a property named `package` that is of string/URL type
{
  "title" : "Product",
  "properties" : {
    "package" : {
      "title" : "_package",
      "readOnly" : false,
      "type" : "string",
      "format" : "uri"
    }
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "http://json-schema.org/draft-04/schema#"
}

However, when creating the entity following the JSON schema, we receive an error:

$ curl http://localhost:8889/products -XPOST -d '{"package": "http://localhost:8889/packages/f34c136f-3004-4908-aa19-169d4b086da0" }' -H 'Content-type: application/json'
{
  "cause": {
    "cause": null,
    "message": "Cannot construct instance of `eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model.Package` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8889/packages/f34c136f-3004-4908-aa19-169d4b086da0')\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 13] (through reference chain: eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model.Product[\"package\"])"
  },
  "message": "JSON parse error: Cannot construct instance of `eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model.Package` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8889/packages/f34c136f-3004-4908-aa19-169d4b086da0'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model.Package` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8889/packages/f34c136f-3004-4908-aa19-169d4b086da0')\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 13] (through reference chain: eu.xenit.contentcloud.userapps.vierbergenlars.acc296.model.Product[\"package\"])"
}

Analysis

We were able to trace the difference between a renamed and non-renamed JSON property to this line:

PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getName());

The issue at hand is that property.getName() returns the name of the property in JSON/serialized format.
In simple cases, this matches with the name of the PersistentProperty as expected by entity.getPersistentProperty().

But when a field is given a different serialized name with @JsonProperty, no PersistentProperty for that name can be found, resulting in no custom deserializer being installed for that property.

Related issues

I found a couple issues that seem to have the same cause:

Reproducer application

acc-296.zip

This is a spring-boot application using spring-data-rest.

  1. Run the application by executing ./gradlew bootRun in the project.
  2. Create a package curl http://localhost:8080/packages -XPOST -d '{}' -H 'Content-type: application/json'
  3. Use the value from _links.self.href to create a product: curl http://localhost:8080/products -XPOST -d '{"package": "http://localhost:8080/packages/f34c136f-3004-4908-aa19-169d4b086da0" }' -H 'Content-type: application/json'

You can also perform the same operations from HAL explorer, entering the package URL in the _package form field, which generates the same {"package": "...."} JSON

Testcase

I added a failing test (and a passing test for the functionality that did not seemed to be covered) in my fork: 3.7.x...vierbergenlars:spring-data-rest:fix-renamed-linkable-assocs

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 27, 2022
vierbergenlars added a commit to vierbergenlars/spring-data-rest that referenced this issue Aug 4, 2022
Normally, when creating and updating (POST/PUT) an entity via the REST endpoints,
you can use the URL of the relation target. (e.g.: send `{"package": "/packages/1"}`
when `package` is a JPA `@OneToOne` relation)

Now this also takes into account when the JPA relation is annotated with
`@JsonProperty()` to change the serialized name.

Fixes spring-projects#2165
vierbergenlars added a commit to vierbergenlars/spring-data-rest that referenced this issue Aug 4, 2022
Normally, when creating and updating (POST/PUT) an entity via the REST endpoints,
you can use the URL of the relation target. (e.g.: send `{"package": "/packages/1"}`
when `package` is a JPA `@OneToOne` relation)

Now this also takes into account when the JPA relation is annotated with
`@JsonProperty()` to change the serialized name.

Fixes spring-projects#2165
vierbergenlars added a commit to vierbergenlars/spring-data-rest that referenced this issue Aug 4, 2022
Normally, when creating and updating (POST/PUT) an entity via the REST endpoints,
you can use the URL of the relation target. (e.g.: send `{"package": "/packages/1"}`
when `package` is a JPA `@OneToOne` relation)

Now this also takes into account when the JPA relation is annotated with
`@JsonProperty()` to change the serialized name.

Closes spring-projects#2165
@mp911de mp911de self-assigned this Oct 17, 2022
@odrotbohm
Copy link
Member

Thanks for the detailed write up. I took a look at this and I think we have to clarify a few things before moving on. I think the property model that both Jackson and Spring Data build for the arrangement you present is not working the way you assume it to work. The internal name not being reported properly seems to be cause by Jackson invalidly connecting the _property field to the getProperty() getter (you disabling the getter for _property could've been a hint at the problem).

To cite your test case:

class PetOwner {

  @Getter(value = AccessLevel.NONE)
  @JsonProperty("package")
  Package _package;

  public Package getPackage() {
    return _package;
  }
}

Spring Data defaults to field access and thus the only property found is named _property backed by the field with the same name. For it, the getPackage() is just an arbitrary method. For some reason, and completely at odds with the JavaBeans specification, Jackson finds a property named package (likely induced by the getter) and connects both the getter getPackage() and the field _package together. This is wrong, needs to be brought up with Jackson, and we shouldn't start adhering to make the connection between unrelated fields and accessor methods.

I'll push a fix that will use our MappedProperties already in place to map Jackson property field names to properties detected by Spring Data, but that in turn needs Jackson to behave properly. That makes your test cases succeed if you remove the manually declared getter. You can also remove the getter disabled setup for _package.

@odrotbohm odrotbohm assigned odrotbohm and unassigned mp911de Feb 21, 2023
@odrotbohm odrotbohm added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 21, 2023
odrotbohm added a commit that referenced this issue Feb 21, 2023
…deserialization.

Revert the changes that employed manual annotation lookup as that would cause invalid associations of fields and accessor methods for properties shadow renamed. Instead, we now use MappedProperties that already contains a mapping between the Jackson field names and Spring Data property names.

Fixes: #2165

$ Conflicts:
$	spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java
$	spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java
odrotbohm pushed a commit that referenced this issue Feb 21, 2023
Normally, when creating and updating (POST/PUT) an entity via the REST endpoints, you can use the URL of the relation target. (e.g.: send `{"package": "/packages/1"}` when `package` is a JPA `@OneToOne` relation).

Now this also takes into account when the JPA relation is annotated with `@JsonProperty` to change the serialized name.

Add unit tests for linkable associations.

Issue: #2165
odrotbohm added a commit that referenced this issue Feb 21, 2023
…deserialization.

Revert the changes that employed manual annotation lookup as that would cause invalid associations of fields and accessor methods for properties shadow renamed. Instead, we now use MappedProperties that already contains a mapping between the Jackson field names and Spring Data property names.

Fixes: #2165

$ Conflicts:
$	spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java
$	spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java
@vierbergenlars
Copy link
Contributor Author

Thank you for the extended explanation.

The original application (which was attached in a zip) does not use such a custom getter, so the fix that you propose should work out fine.

It's been a while, but I believe the reason why I added a getPackage() method is because I wanted the entity model to match with the "specification" for the entity that I have, which uses the name package for this relation.
In theory, that name would be perfectly fine, except for Java recognizing package as a keyword that can of course not be used as an identifier. So I tried to work around that with a getter that makes the API for the PetOwner object look like it contains a package field.

In our actual production applications, this is not a concern, since only the REST API is part of the public API.

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

Successfully merging a pull request may close this issue.

4 participants