When I try to parse the following object:
@Data
public final class User implements Serializable {
@JsonProperty("alias")
private final String alias;
}
I get the error
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `microservices.book.multiplication.domain.User` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `microservices.book.multiplication.domain.User` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 10]
And I solved this by adding a creator to the User class like this:
@Data
public final class User implements Serializable {
@JsonProperty("alias")
private final String alias;
@JsonCreator
public User(@JsonProperty("alias") String alias){
this.alias = alias;
}
}
This works fine.
So now I go and parse the following object:
@Data
public final class Multiplication implements Serializable {
@JsonProperty("factorA")
private final Integer factorA;
@JsonProperty("factorB")
private final Integer factorB;
}
and I don't get an error... why? why it doesn't require me to use a constructor here?
Here's the class where the parsing takes place
package microservices.book.multiplication.controller;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.controller.MultiplicationResultAttemptController.ResultResponse;
import microservices.book.multiplication.domain.User;
import microservices.book.multiplication.service.MultiplicationService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@WebMvcTest(MultiplicationResultAttemptController.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MultiplicationResultAttemptControllerTest {
@MockBean
MultiplicationService multiplicationService;
@Autowired
private MockMvc mvc;
private JacksonTester<MultiplicationResultAttempt> jsonResult;
private JacksonTester<ResultResponse> jsonResponse;
@BeforeAll
void initAll(){
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void postResultReturnCorrect() throws Exception{
genericParameterizedTest(true);
}
@Test
public void postResultReturnNotCorrect() throws Exception{
genericParameterizedTest(true);
}
void genericParameterizedTest(final Boolean correct) throws Exception{
given(multiplicationService.checkAttempt(any(MultiplicationResultAttempt.class))).willReturn(correct);
User user = new User("Smith");
Multiplication multiplication = new Multiplication(50,70);
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(user, multiplication, 3500);
// when
MockHttpServletResponse response = mvc.perform(
post("/results").contentType(MediaType.APPLICATION_JSON).
content(jsonResult.write(attempt).getJson())).
andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonResponse.write(new ResultResponse(correct)).getJson());
}
}
CodePudding user response:
The property alias
of your User
is marked as final
, which means it can't be initialized through a setter. Because in order to assign a value to the field through a setter it should be already initialized with the default value (null
, 0
, false
depending on the type), but final
variable can't be reassigned.
Reminder: the mechanism which Jackson uses by default is to instantiate the class using no-args constructor and then assign the fields via setters. But it can't be done if one of the fields is final
, they such fields need to be provided through constructor, because final
means - "assign only once".
There are two ways to instruct Jackson to use a parametrized constructor:
1. The first option is to point at the constructor explicitly by annotating it with @JsonCreator
and apply @JsonProperty
on each of the constructor arguments as specified in the documentation.
2. Another option is to add a dependency to Jackson Modules: Java 8 (link to Maven repository), which is an umbrella multi-module that consists of several modules including Parameter names module.
And then you need to configure ObjectMapper
. For that, you need to place ParameterNamesModule
as a Bean into the Spring's Context, and it would be grabbed at the application start-up and applied while ObjectMapper
would be configured.
@Bean
public ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.PROPERTIES);
}
But there's a caveat. A quote from the documentation of the JsonCreator.Mode.PROPERTIES
:
Mode that indicates that the argument(s) for creator are to be bound from matching properties of incoming Object value, using creator argument names (explicit or implicit) to match incoming Object properties to arguments.
Note that this mode is currently (2.5) always used for multiple-argument creators; the only ambiguous case is that of a single-argument creator.
I.e. this option would not work if your constructor has exactly one argument.
To make the Jackson recognize a single-arg constructor, you need to apply @JsonProperty
on the argument.
This behavior explained here and also mentioned in the module-description at the very bottom of the page:
Preconditions:
- if class Person has a single argument constructor, its argument needs to be annotated with
@JsonProperty("propertyName")
. This is to preserve legacy behavior