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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With