Skip to content

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

@vierbergenlars

Description

@vierbergenlars

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions