spring-projects / spring-projects/spring-data-rest
Polymorphic update for PUT does not update type if changed from subclass to superclass
@odrotbohm is already working on this.
Since Apr 17, 2023.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.