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 Item
s. 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 a HandlerInstantiator
:
My code from above would then be amended like this, the crucial line marked with //!!
.
@Autowired
private LinkRelationProvider linkRelationProvider;
@Autowired
private MessageResolver messageResolver
// ...
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(
linkRelationProvider, CurieProvider.NONE, messageResolver)); //!!
ObjectWriter ow = objectMapper.writer().withDefaultPrettyPrinter();
String itemSerialized = ow.writeValueAsString(testItem);
// ...
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.