-
Notifications
You must be signed in to change notification settings - Fork 563
Description
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:
Line 447 in 6030f85
| 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:
- Annotating getter with @JsonProperty breaks EntityLookup for POST request. #1794
- DATAREST-1232 - Fix AssociationUriResolving with snake case properties #291
- Deserialize snake case JSON does not work for association fields #1591
- POST association URL fails for custom Jackson naming strategy #992
Reproducer application
This is a spring-boot application using spring-data-rest.
- Run the application by executing
./gradlew bootRunin the project. - Create a package
curl http://localhost:8080/packages -XPOST -d '{}' -H 'Content-type: application/json' - Use the value from
_links.self.hrefto 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