Skip to content

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

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

Open
mjustin opened this issue Apr 20, 2022 · 0 comments
Assignees
Labels
status: waiting-for-triage An issue we've not yet triaged

Comments

@mjustin
Copy link

mjustin commented Apr 20, 2022

I was looking at #2130 and its commit (adcd7e7). 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
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

No branches or pull requests

3 participants