Search code examples
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:

    • StudentCreationDTO1 is not deserialized as it provides only an @AllArgsConstructor and no default constructor. In fact, an InvalidDefinitionException is thrown.

    • StudentCreationDTO2 is deserialized as it provides a default constructor.

    • StudentCreationDTO3 is deserialized as it provides a (constructor) method annotated with @JsonCreator. The annotation doesn't need to include all the class' fields, but only a few of them, just so that Jackson is able to create an instance of StudentCreationDTO3 and then set the remaining fields.

    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\": \"[email protected]\"\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.

    • If a property has a setter, then its value is set via the corresponding setter method.
    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;
        }
    }
    
    • If no setter is provided but the class exhibits a method annotated with @JsonSetter and the name of the Json property, then Jackson assumes that the annotated method is the right way to set the json property.
    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;
        }
    }
    
    • If no setter or @JsonSetter method is provided, but the class exhibits only getter methods, then Jackson falls back on reflection to set the object's properties.
    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;
        }
    }
    
    • If a class doesn't provide any getter or setter, by default, Jackson sets only public fields or any field whose visibility is equal or above the one set with the method ObjectMapper.setVisibility().
    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 ....
    }