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);
}