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?
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\": \"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");
}
}
}
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;
}
}
@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;
}
}
@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;
}
}
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 ....
}