Jackson vs. Spring HATEOAS vs. Polymorphism

839 views Asked by At

When I want to deserialize an Entity with a polymorph member, Jackson throws a com.fasterxml.jackson.databind.JsonMappingException, complaining about a missing type info (...which is actually present in the JSON -> see example).

Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '@class' that is to contain type id  (for class demo.animal.Animal)\n at [Source: N/A; line: -1, column: -1] (through reference chain: demo.home.Home[\"pet\"])"

All actual work is done by a PagingAndSortingRepository from Spring HATEOAS.

I use spring-boot V 1.2.4.RELEASE, which means jackson is V 2.4.6 and Spring HATEOAS is V 0.16.0.RELEASE.

Example:

I have a pet at home:

@Entity
public class Home {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @OneToOne(cascade = {CascadeType.ALL})
    private Animal pet;

    public Animal getPet() {
        return pet;
    }

    public void setPet(Animal pet) {
        this.pet = pet;
    }

}

That Pet is some Animal - in this case either a Cat or a Dog. It's type is identified by the @class property...

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public abstract class Animal {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

@Entity
public class Cat extends Animal {

}

@Entity
public class Dog extends Animal {

}

Then there is this handy PagingAndSortingRepository, which allows me to access my home via REST/HATEOAS...

@RepositoryRestResource(collectionResourceRel = "home", path = "home")
public interface HomeRepository extends PagingAndSortingRepository<Home, Integer> {

}

To confirm all that stuff is working, I have a test in place...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
@WebAppConfiguration
public class HomeIntegrationTest {

    @Autowired
    private WebApplicationContext ctx;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        this.mockMvc = webAppContextSetup(ctx).build();
    }

    @Test
    public void testRename() throws Exception {

        // I create my home with some cat...
        // http://de.wikipedia.org/wiki/Schweizerdeutsch#Wortschatz -> Büsi
        MockHttpServletRequestBuilder post = post("/home/")
                .content("{\"pet\": {\"@class\": \"demo.animal.Cat\", \"name\": \"Büsi\"}}");
        mockMvc.perform(post).andDo(print()).andExpect(status().isCreated());

        // Confirm that the POST request works nicely, so the JSON thingy is correct...
        MockHttpServletRequestBuilder get1 = get("/home/").accept(MediaType.APPLICATION_JSON);
        mockMvc.perform(get1).andDo(print()).andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$._embedded.home", hasSize(1)))
                .andExpect(jsonPath("$._embedded.home[0].pet.name", is("Büsi")));

        // Now the interesting part: let's give that poor kitty a proper name...
        MockHttpServletRequestBuilder put = put("/home/1")
                .content("{\"pet\": {\"@class\": \"demo.animal.Cat\", \"name\": \"Beauford\"}}");
        mockMvc.perform(put).andDo(print()).andExpect(status().isNoContent());
        // PUT will thow JsonMappingException exception about an missing "@class"...

        MockHttpServletRequestBuilder get2 = get("/home/").accept(MediaType.APPLICATION_JSON);
        mockMvc.perform(get2).andDo(print()).andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$._embedded.home", hasSize(1)))
                .andExpect(jsonPath("$._embedded.home[0].pet.name", is("Beaufort")));

    }

}

Interestingly I can create my home with the cat as a pet, but when I want to update the name of the cat it cannot deserialize the JSON anymore...

Any suggestions?

1

There are 1 answers

0
ci_ On

I'm going to attempt a half-answer.

When processing a PUT (probably PATCH as well), spring-data-rest-webmvc merges the given JSON data into the existing entity. While doing so, it strips all properties that don't exist in the entity from the JSON tree before passing it to the Jackson ObjectMapper. In other words, your @class property is gone by the time Jackson gets to deserialize your object.

You can work around this (for testing/demonstration purposes) by adding your @class property as an actual property to your entity (you have to rename it of course, say classname). Now everything will work fine, however your entity now has an otherwise useless classname property, which is probably not what you want.

Using the @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.WRAPPER_OBJECT) approach also won't work, for a similar reason (except this time the entire wrapper object is removed). Also as with the original approach, GET and POST will work fine.

The whole thing looks like a bug or @JsonTypeInfo not supported in spring-data-rest-webmvc situation.

Maybe somebody else can shed some more light on this.