In a Spring Boot project, I am trying to serialize and deserialize related objects using HATEOAS in hal+json format. I'm using org.springframework.boot:spring-boot-starter-hateoas to do so and it works great in my controller:
@GetMapping(path = "/{id}", produces = MediaTypes.HAL_JSON_VALUE + ";charset=UTF-8")
HttpEntity<Item> getItemById(@PathVariable Integer id) {
Item item = itemRepository.findById(id).get();
item.add(linkTo(ItemController.class).slash(item.getId()).withSelfRel());
item.add(linkTo(methodOn(CategoryController.class).getCategoryById(item.getCategory().getId()))
.withRel("category"));
return new HttpEntity<>(item);
}
With the Item class being:
@Entity
public class Item extends RepresentationModel<Item> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "item_id")
private Integer id;
@ManyToOne
@JoinColumn(name = "category_id", referencedColumnName = "category_id")
@JsonIgnore
private Category category;
private String name;
// Getters and Setters are present but not entered here in the question
}
Calling the GET method will result in something like
{
"name": "foo"
"_links": {
"self": {
"href": "http://localhost:8080/items/1"
},
"category": {
"href": "http://localhost:8080/categories/2"
}
},
"item_id": 1
}
This is just what I want.
The problem occurs now when I'm trying to write a test for the POST method which is intended to be used for creating new Items. This is what my test looks like:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@AutoConfigureMockMvc
public class ItemControllerTest{
@Autowired
private ObjectMapper objectMapper;
@Autowired
private MockMvc mockMvc;
@LocalServerPort
private int port;
@Test
void testCreate() throws Exception {
Item testItem = new Item("test");
testItem.add(Link.of("http://localhost:" + port + "/categories/2", "category"));
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
ObjectWriter ow = objectMapper.writer().withDefaultPrettyPrinter();
String itemSerialized = ow.writeValueAsString(testItem);
mockMvc.perform(post("/items/")
.content(itemSerialized)
.contentType(MediaTypes.HAL_JSON_VALUE))
.andExpect(status().isOK());
}
}
This serialization does not work as needed, as the relations look like this in the itemSerialized String:
"links" : [{
"rel" : "category",
"href" : "http://localhost:61524/categories/2"
}]
(What would be correct is _links with the underscore and a category relation structure like given above.)
As I understood, the cause of this is, that the objectMapper I use is not aware of the need to serialize to json+hal format. (Which correctly happens by default/magic in the Controller when it returns the Item from getItemById.) Now, what would be the proper way to ensure, that the correct serialization happens in the test as well?
I have read about using objectMapper.registerModule(new Jackson2HalModule());, which seems reasonable to me. But when I use it, an exception occurs:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'org.springframework.hateoas.mediatype.hal.Jackson2HalModule$HalLinkListSerializer':
Failed to instantiate [org.springframework.hateoas.mediatype.hal.Jackson2HalModule$HalLinkListSerializer]:
No default constructor found
Any help would be greatly appreciated!
FTR, I found a solution after searching and trying a lot. Mainly this answer led me the right way. The key is not only to call
objectMapper.registerModule(new Jackson2HalModule());, but also to supply aHandlerInstantiator:My code from above would then be amended like this, the crucial line marked with
//!!.This way, no error occurs and the result looks like I want it to. Anyway, I don't know if this is the best possible solution or why Spring Hateoas doesn't initialize things correctly at test time as it does at normal runtime.