Java
Jackson의 모든 것 - Optional
Jaime.Lee
2019. 12. 4. 10:33
모든 소스는 여기서 확인하실 수 있습니다.
Optional을 Jackson을 이용해 Serialize/Deserialize하면 어떻게 될까요?
Optional 필드를 가지는 Object
하나의 Optional 필드를 가지는 Name이라는 클래스를 생성하였습니다.
package io.lcalmsky.jackson.domain;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Optional;
public class BeanWithOptional {
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class Name {
private String firstName;
private String lastName;
private Optional<String> nickname;
}
}
Optional을 필드변수로 사용하는 것은 권장하는 사항이 아니라 단순 테스트를 위함입니다.
직렬화(Serialization)
package io.lcalmsky.jackson.domain;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
public class BeanWithOptionalTests {
@Test
public void givenBeanWithOptional_whenSerialize_thenCorrect() throws JsonProcessingException {
// given
BeanWithOptional.Name name = new BeanWithOptional.Name();
name.setFirstName("Lionel");
name.setLastName("Messi");
name.setNickname(Optional.of("GOAT"));
// when
String json = new ObjectMapper().writeValueAsString(name);
// then
assertThat(json, not(containsString("GOAT")));
System.out.println(json);
}
}
{"firstName":"Lionel","lastName":"Messi","nickname":{"present":true}}
Process finished with exit code 0
Optional 필드 출력시 값이 포함되지 않고 present라는 필드가있는 중첩 JSON 객체가 포함되어 있음을 확인할 수 있습니다.
그 이유는 Optional 클래스의 public getter 메소드가 isPresent이기 때문입니다. 따라서 직렬화시에 nickname 필드에 값이 있으면 true, 없으면 false가 present라는 필드에 저장됩니다.
다시 아래와 같이 테스트를 해보면,
package io.lcalmsky.jackson.domain;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BeanWithOptionalTests {
private String json;
@Test
@DisplayName("Optional 필드를 직렬화하면 값이 포함되지 않음")
public void test1() throws JsonProcessingException {
// given
BeanWithOptional.Name name = givenName("GOAT");
// when
json = whenSerialize(name);
// then
assertFalse(() -> json.contains("GOAT"));
}
@Test
@DisplayName("Optional 필드를 직렬화하면 isPresent의 값이 포함됨")
public void test2() throws JsonProcessingException {
// given
BeanWithOptional.Name name = givenName("GOAT");
// when
json = whenSerialize(name);
// then
assertTrue(() -> json.contains("true"));
}
@Test
@DisplayName("Optional 필드를 null로 지정하면 isPresent값이 false로 지정됨")
public void test3() throws JsonProcessingException {
// given
BeanWithOptional.Name name = givenName(null);
// when
json = whenSerialize(name);
// then
assertTrue(() -> json.contains("false"));
}
private String whenSerialize(BeanWithOptional.Name name) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(name);
}
private BeanWithOptional.Name givenName(String nickname) {
BeanWithOptional.Name name = new BeanWithOptional.Name();
name.setFirstName("Lionel");
name.setLastName("Messi");
name.setNickname(Optional.ofNullable(nickname));
return name;
}
@AfterEach
public void teardown() {
System.out.println(json);
}
}
원하는 테스트 결과를 얻을 수 있습니다.
역직렬화(Deserialization)
반대로 실제 값을 대입하여 역직렬화를 하였을 때는 JsonMappingException이 발생합니다.
@Test
@DisplayName("Optional 필드에 값이 포함된 JSON을 역직렬화하면 예외가 발생함")
public void givenJsonWithRealValue_whenDeserialize_thenThrowsJsonMappingException() throws JsonProcessingException {
// given
json = "{\"firstName\":\"Lionel\",\"lastName\":\"Messi\",\"nickname\":\"GOAT\"}";
// when & then
assertThrows(
JsonMappingException.class,
() -> new ObjectMapper().readValue(json, BeanWithOptional.Name.class)
);
}
본질적으로 Jackson은 nickname 값을 인수로 사용하는 생성자가 필요하지만 Optional 필드는 해당되지 않습니다.
Optional 필드가 포함된 클래스를 역직렬화 하기 위해선 @JsonSetter 어노테이션 등을 이용하여 역직렬화시 사용할 메서드를 지정해주시면 됩니다.
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public static class NameWithOptionalSetter {
private String firstName;
private String lastName;
private Optional<String> nickname;
@JsonSetter
public void setNickname(String nickname) {
this.nickname = Optional.ofNullable(nickname);
}
}
@Test
@DisplayName("Optional 필드에 값이 포함된 JSON을 setter를 지정하여 역직렬화하면 성공함")
public void givenJsonWithRealValueAndSetter_whenDeserialize_thenSuccess() throws JsonProcessingException {
// given
json = "{\"firstName\":\"Lionel\",\"lastName\":\"Messi\",\"nickname\":\"GOAT\"}";
// when
BeanWithOptional.NameWithOptionalSetter name = new ObjectMapper().readValue(json, BeanWithOptional.NameWithOptionalSetter.class);
// then
assertTrue(() -> name.getNickname().filter("GOAT"::equals).isPresent());
System.out.println(name);
}