spring-projects / spring-projects/spring-data-rest

Polymorphic update for PUT does not update type if changed from subclass to superclass

Open
#2,137 1 comment 0 reactions 1 assignee View on GitHub

@odrotbohm is already working on this.

Since Apr 17, 2023.

status: waiting-for-triage
Dominant language
Java
Stars
958
Forks
568
PR merge metrics
No merged PRs in 30d

Description

I was looking at #2130 and its commit (adcd7e74efb696b34b7e516556cb6282bf66befd). This fixed the situation for a PUT request when the type changes from a superclass to a subclass (e.g. Animal => Bird), such that the saved entity would correctly be the new subclass type. The implementation (an instance check) got me suspicious that inverse would not work correctly: that changing from the subtype to the supertype would not change the type of the object. I checked it out using Spring Data MongoDB, and my suspicion proved to be correct.

Expected behavior

When updating a REST entity using PUT, the Jackson type of the result should be used.

Actual behavior

When updating a REST entity using PUT, the Jackson type of the result is not used when the type of the request (per Jackson) is a supertype of the type of the existing saved entity. Instead, the REST entity type remains unchanged as the subclass type.

Example

Given the following Spring Data MongoDB document classes:

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION, defaultImpl = Animal.class)
@JsonSubTypes({@JsonSubTypes.Type(Bird.class)})
@Document
public class Animal {
    @Id
    private String id;
    private String named;

    public Animal() { }
    public Animal(String named) { this.named = named; }

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

    public String getNamed() { return named; }
    public void setNamed(String named) { this.named = named; }
}
@Document
public class Bird extends Animal {
    public Integer airSpeedVelocity;

    public Bird() { }
    public Bird(String named, Integer airSpeedVelocity) {
        super(named);
        this.airSpeedVelocity = airSpeedVelocity;
    }

    public Integer getAirSpeedVelocity() { return airSpeedVelocity; }
    public void setAirSpeedVelocity(Integer airSpeedVelocity) { this.airSpeedVelocity = airSpeedVelocity; }
}

This uses Jackson deduction-based polymorphism such that if an instance has an airSpeedVelocity, it is a Bird; if it does not, it is a non-bird Animal. For example, {"name": "Swallow", "airSpeedVelocity": 20} is treated by Jackson as a Bird, but {"name": "Cat"} is treated as a non-bird Animal.

Tests

Given the following two tests, updateAnimalToBird passes, but updateBirdToAnimal fails.

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
class AnimalRepositoryRestIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private AnimalRepository animalRepository;

    @Autowired
    private JacksonTester<Animal> jsonTester;

    @Test
    void updateAnimalToBird() throws Exception {
        Animal animal = new Animal("Elephant");
        Bird bird = new Bird("Swallow", 20);

        Animal saved = animalRepository.save(animal);

        mockMvc.perform(put("/animals/{id}", saved.getId()).content(jsonTester.write(bird).getJson()));

        Animal retrieved = animalRepository.findById(saved.getId()).orElseThrow();

        assertEquals(Bird.class, retrieved.getClass());
        assertEquals(20, ((Bird) retrieved).getAirSpeedVelocity());
    }

    @Test
    void updateBirdToAnimal() throws Exception {
        Animal animal = new Animal("Cat");
        Bird bird = new Bird("Pigeon", 15);

        Animal saved = animalRepository.save(bird);

        mockMvc.perform(put("/animals/{id}", saved.getId()).content(jsonTester.write(animal).getJson()));

        Animal retrieved = animalRepository.findById(saved.getId()).orElseThrow();

        assertEquals(Animal.class, retrieved.getClass()); // Fails; as this is Bird
    }
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.