Home > Net >  When is a @JsonCreator method required for parsing?
When is a @JsonCreator method required for parsing?

Time:10-29

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
  • Related