javaspringspring-mvccontrollerjackson

Why Jackson needs a default constructor?


I am using Java Spring Boot for my project and I have the following controller:

@AllArgsConstructor
@RestController
@RequestMapping("/api/subject")
public class SubjectController {
    private SubjectService subjectService;

    @PostMapping
    public void createSubject(@RequestBody SubjectCreationDTO subjectCreationDTO) {
        LoggingController.getLogger().info(subjectCreationDTO.getTitle());
//        subjectService.createSubject(subjectCreationDTO);
    }
}

And SubjectCreationDTO:

@AllArgsConstructor
@Getter
@Setter
public class SubjectCreationDTO {
    private String title;
}

So I get this error when making a POST request:

JSON parse error: Cannot construct instance of pweb.examhelper.dto.subject.SubjectCreationDTO (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)"

I can solve this error by adding @NoArgsConstructor to SubjectCreationDTO, but why is this necessary, when in other cases, I have the almost exactly the same case.

@PostMapping
public ResponseEntity<StudentDTO> createStudent(@RequestBody StudentCreationDTO studentCreationDTO) {
    StudentDTO savedStudent = studentService.createStudent(studentCreationDTO);
    return new ResponseEntity<>(savedStudent, HttpStatus.CREATED);
}

and this is the StudentCreationDTO class:

@AllArgsConstructor
@Getter
@Setter
public class StudentCreationDTO {
    private String username;
    private String firstName;
    private String lastName;
    private String email;
}

I have figured it out that in case of having more than just one field, you do not have to specify @NoArgsConstructor and Jackson Library can parse the input JSON from the body just as fine. My question is why it has this behavior, and why it can't parse if I have only one field in the class without the default constructor, but it can if I have multiple fields?


Solution

  • In order for Jackson to deserialize a Json, it either needs a default constructor or a method annotated with @JsonCreator. Without any of these two methods, Jackson is not able to instantiate an instance and raises an InvalidDefinitionException. This is what the error cannot deserialize from Object value (no delegate- or property-based Creator) is trying to say.

    With a default constructor, Jackson first creates a default instance of the class and then injects the object's properties with each field read from the Json.

    Likewise, with the @JsonCreator approach, Jackson first instantiates an object with only the properties specified as the parameters of the method annotated with @JsonCreator. Then, sets each remaining field from the Json into the object. The annotated method can be either a parameterized constructor or a static method.

    Normally, you shouldn't be able to deserialize an object with just an @AllArgsContructor, but there must be some other configuration that handles a parameterized instantiation for you. Here is also an article from Baeldung where at point 10.1 shows a typical case of a class not being deserialized because it lacks of both a default constructor or a method annotated with @JsonCreator.

    I've also attached an example that you can try at oneCompiler where it shows how Jackson behaves when there is only a parameterized constructor and no default constructor or @JsonCreator method. Precisely, the example handles the following scenarios:

    public class Main {
        public static void main(String[] args) throws JsonProcessingException {
            String json = "{\n" +
                    "\t\"username\": \"johndoe\",\n" +
                    "\t\"firstName\": \"john\",\n" +
                    "\t\"lastName\": \"doe\",\n" +
                    "\t\"email\": \"john.doe@mail.com\"\n" +
                    "}";
            ObjectMapper objectMapper = new ObjectMapper();
    
            //Deserializing with no default constructor
            try {
                StudentCreationDTO1 studentCreationDTO1 = objectMapper.readValue(json, StudentCreationDTO1.class);
                System.out.println(studentCreationDTO1);
            } catch (InvalidDefinitionException e) {
                System.out.println("Throwing InvalidDefinitionException because there is no default constructor or method marked with @JsonCreator");
            }
    
            //Deserializing with default constructor
            try {
                StudentCreationDTO2 studentCreationDTO2 = objectMapper.readValue(json, StudentCreationDTO2.class);
                System.out.println("\n" + studentCreationDTO2);
            } catch (InvalidDefinitionException e) {
                System.out.println("Throwing InvalidDefinitionException because there is no default constructor or method marked with @JsonCreator");
            }
    
            //Deserializing with no default constructor but with method annotated with @JsonCreator
            try {
                StudentCreationDTO3 studentCreationDTO3 = objectMapper.readValue(json, StudentCreationDTO3.class);
                System.out.println("\n" + studentCreationDTO3);
            } catch (InvalidDefinitionException e) {
                System.out.println("Throwing InvalidDefinitionException because there is no default constructor or method marked with @JsonCreator");
            }
        }
    }
    

    Further Notes on Jackson Deserializtion

    This is just an extra section that furthers how the deserialization process works, and shows why Jackson needs a first instance in order to read and set values from a Json. I'm also linking a great article from Baeldung that addresses all the following cases for both serialization and deserialization.

    public class MyBean {
        private String name;
    
        //... default constructor ....
    
        //... standard getName() ...
    
        //Jackson uses the corresponding setter to set the property name
        public void setName(String name) {
            this.name = name;
        }
    }
    
    public class MyBean {
        private String name;
    
        //... default constructor ....
    
        //... standard getName() ...
    
        //Marking the following method with @JsonSetter because
        //The json contains a property called name (value = "name"), 
        //but Jackson can't find any setter method in the form setName()
        @JsonSetter(value = "name")
        public void setTheName(String name) {
            this.name = name;
        }
    }
    
    public class MyBean {
        private String name;
    
        //... default constructor ....
    
        //Jackson cannot set a value with just a getter,
        //so it falls falls back on reflection to set name
        public void getName() {
            return name;
        }
    }
    
    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
    
    ...
    
    public class MyBean {
        //Every field is set by Jackson with Visibility.ANY
        public String name;
        proteced int id;
        float value;
        private boolean flag;
    
        //... default constructor ....
    
        //... No getters or setters ....
    }