Spring Boot Web - Object Mapper

Seunghwan Choi·2024년 10월 29일

Java Backend

목록 보기
7/16

The Jackson library's ObjectMapper is a core class for reading and writing JSON in Java applications. It's widely used in Spring Boot and other Java frameworks for serializing Java objects to JSON and deserializing JSON into Java objects.

@SpringBootTest
class RestApiApplicationTests {

	@Autowired
	private ObjectMapper objectMapper;

	@Test
	void contextLoads() throws JsonProcessingException {
		var user = new UserRequest();
		user.setUserName("Choi");
		user.setUserAge(24);
		user.setEmail("choi@gmail.com");
		user.setIsKorean(true);

		var json = objectMapper.writeValueAsString(user);
		System.out.println(json);
        //prints: {"user_name":"Choi","user_age":24,"email":"choi@gmail.com","is_korean":true}


		var dto = objectMapper.readValue(json, UserRequest.class);
		System.out.println(dto);
        //prints: UserRequest(userName=Choi, userAge=24, email=choi@gmail.com, isKorean=true)
	}

}
  • The objectMapper converts the user object into a JSON and it can convert a JSON into a user object.
  • The objectMapper utilizes the getter and setter methods of the class we are converting to/from. So if the class is not annotated with @Data (which automatically generates all the get/set methods) OR if the class does not have getters and setters explicitly declared without using Lombok, the objectMapper will not be able to serialize and deserialize JSONs and DTOs.
//@Data  => Commented out to remove default getters/setters
@AllArgsConstructor
//@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String userName;
    private Integer userAge;
    private String email;
    private Boolean isKorean; //default is false
}

	@Test
	void contextLoads() throws JsonProcessingException {
		var user = new UserRequest("Choi", 24, "choi@gmail.com", true);
//		user.setUserName("Choi");
//		user.setUserAge(24);
//		user.setEmail("choi@gmail.com");
//		user.setIsKorean(true);
		System.out.println(user);

		var json = objectMapper.writeValueAsString(user);
		System.out.println(json);

		//var dto = objectMapper.readValue(json, UserRequest.class);
		//System.out.println(dto);
	}
    
/*printed:
com.example.rest_api.model.UserRequest@74a58a06 (from System.out.println(user); )
{} (from System.out.println(json);)
*/
  • Since the UserRequest class now does not have any getters/setters, the objectMapper fails to create JSON. And since without @Data annotation, there is no toString() method as well, it prints hash code of the UserRequest object instead of printing out its variables.
  • After generating toString() method as shown below, we can see that it prints out all of its variables.
//@Data
@AllArgsConstructor
//@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String userName;
    private Integer userAge;
    private String email;
    private Boolean isKorean; //default is false

    @Override
    public String toString() {
        return "UserRequest{" +
                "userName='" + userName + '\'' +
                ", userAge=" + userAge +
                ", email='" + email + '\'' +
                ", isKorean=" + isKorean +
                '}';
    }
}

/*
prints:
UserRequest{userName='Choi', userAge=24, email='choi@gmail.com', isKorean=true}
*/
  • What if we manually add getter methods but with different naming conventions or with custom method names?
    public String name(){
        return this.userName;
    }

    public int humanAge(){
        return this.userAge;
    }
  • The result remains the same as below, because the way objectMapper works is that it looks for methods starting with "get" and then converts the values returned from those getters, followed by converting the camelCase to snake_case.
UserRequest{userName='Choi', userAge=24, email='choi@gmail.com', isKorean=true}
{}
  • So if we modify the above custom getters as below:
    public String getName(){
        return this.userName;
    }

    public int getHumanAge(){
        return this.userAge;
    }

/* the output we get is:
{"name":"Choi","human_age":24}
*/
  • And now if we add all the valid getters as below:
//@Data
@AllArgsConstructor
//@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String userName;
    private Integer userAge;
    private String email;
    private Boolean isKorean; //default is false

    public String getUserName() {
        return userName;
    }

    public Integer getUserAge() {
        return userAge;
    }

    public String getEmail() {
        return email;
    }

    public Boolean getKorean() {
        return isKorean;
    }
    
//    public String getName(){
//        return this.userName;
//    }
//
//    public int getHumanAge(){
//        return this.userAge;
//    }

    @Override
    public String toString() {
        return "UserRequest{" +
                "userName='" + userName + '\'' +
                ", userAge=" + userAge +
                ", email='" + email + '\'' +
                ", isKorean=" + isKorean +
                '}';
    }
}
  • The output we get is:
{"user_name":"Choi","user_age":24,"email":"choi@gmail.com","korean":true}
  • Notably, isKorean was converted to "korean" because the get method created was getKorean() instead of getIsKorean(). Once we modify getKorean() to getIsKorean(), we can see below that we are finally getting the desired JSON object.
{"user_name":"Choi","user_age":24,"email":"choi@gmail.com","is_korean":true}
  • To conclude, when objectMapper serializes an object into a JSON, it utilizes the get methods in the class. The member variable names do not matter here. If we add another get method like below:
public String getUser(){
	return userName;
}
/* output:
{"user_name":"Choi","user_age":24,"email":"choi@gmail.com","is_korean":true,"user":"Choi"}
*/

since objectMapper utilized getUser() class, converted User into snake_case, before adding it to the JSON object. If we want to keep getUser() method but we do not want our serialized JSON object to contain key "user" like above, we can add an annotation to the getUser() method, so that objectMapper ignores this get method while serializing.

@JsonIgnore
public String getUser(){
	return this.userName;
}

Or if we want, for example, the variable "email" to be "user_email" in the JSON, we can add annotation to the member variable declaration as below:

@JsonProperty("user_email")
private String email;

/*
This causes the email variable to be named as "user_email" in the Json, printing below:
{"user_name":"Choi","user_age":24,"is_korean":true,"user_email":"choi@gmail.com"}
  • Deserializing:
  • For testing, we get rid of @Data annotation, and block the default constructor, as well as AllArgsConstructor.
//@Data
//@AllArgsConstructor
//@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String userName;
    private Integer userAge;
    private String email;
    private Boolean isKorean; //default is false
    
    private UserRequest(){}


    @Override
    public String toString() {
        return "UserRequest{" +
                "userName='" + userName + '\'' +
                ", userAge=" + userAge +
                ", email='" + email + '\'' +
                ", isKorean=" + isKorean +
                '}';
    }
}

var json = "{\"user_name\":\"Choi\",\"user_age\":24,\"email\":\"choi@gmail.com\",\"is_korean\":true}";
System.out.println(json);
/*prints: {"user_name":"Choi","user_age":24,"email":"choi@gmail.com","is_korean":true}
*/

var dto = objectMapper.readValue(json, UserRequest.class);
System.out.println(dto);
/*
prints: UserRequest{userName='null', userAge=null, email='null', isKorean=null}
*/
  • The objectMapper is unable to convert Json to an object, since objectMapper uses a class's setter methods while deserializing into an object. So if we add @Setter annotation, which adds setters for all of the class's member variables, we can see that it manages to successfully convert the JSON into an object:
@Setter
public class UserRequest{
//...
}

/*
Result:
UserRequest{userName='Choi', userAge=24, email='choi@gmail.com', isKorean=true}
*/
  • Interestingly, even if we only have getters by annotating the class with @Getter, we can still see that the objectMapper successfully deserializes the Json into an object.
    - This is because objectMapper looks for either a default no-args constructor or a constructor that takes all required arguments.
    • When there is no public default no-args constructor nor setter methods, it still manages to bind the member variables accurately even when there are getters.
  • Additionally, even when a class does not have both getters and setters, we can still have objectMapper correctly deserializing Json into an object by having @JsonProperty("..") annotation:
    @JsonProperty("user_name")
    private String userName;
    
    @JsonProperty("user_age")
    private Integer userAge;
    
    @JsonProperty("email")
    private String email;
    
    @JsonProperty("is_korean")
    private Boolean isKorean; //default is false

But the best practice is to use the default method, without blocking @Data, @AllArgsConstructor or @NoArgsConstructor. And if there is a need to specially match a variable to a different name, we just use @JsonProperty("..").

0개의 댓글