Java

Jackson의 모든 것 - 역 직렬화 편

Jaime.Lee 2019. 11. 19. 16:29
모든 소스는 여기서 확인하실 수 있습니다.

@JsonCreator

역 직렬화에 사용되는 생성자나 팩토리를 조정할 수 있습니다.
필요한 대상 엔터티와 정확히 일치하지 않는 일부 JSON을 역 직렬화해야 할 때 유용합니다.

아래와 같은 JSON이 존재하고

{
  "id": 1,
  "theName": "beanName"
}

대상 엔터티에는 "theName" 필드가 존재하지 않지만 엔터티 자체를 변경하고 싶지 않을 때 생성자에 @JsonCreator주석을 달고  @JsonProperty를 사용하여 해결할 수 있습니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class BeanWithJsonCreator {
    private int id;
    private String name;

    @JsonCreator
    public BeanWithJsonCreator(
            @JsonProperty("id") int id,
            @JsonProperty("theName") String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("BeanWithJsonCreator{");
        sb.append("id=").append(id);
        sb.append(", name='").append(name).append('\'');
        sb.append('}');
        return sb.toString();
    }
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class BeanWithJsonCreatorTests {
    @Test
    @DisplayName("@JsonCreator를 이용하여 역직렬화")
    public void givenJsonCreatorApplied_whenDeserialize_thenCorrect() throws JsonProcessingException {
        // given
        String json = "{\"id\":1,\"theName\":\"beanName\"}";

        // when
        BeanWithJsonCreator bean = new ObjectMapper().readValue(json, BeanWithJsonCreator.class);

        // then
        assertEquals("beanName", bean.getName());
        assertEquals(1, bean.getId());

        // log
        System.out.println(json);
        System.out.println(bean);
    }
}

JUnit 테스트 결과

@JacksonInject

JSON 데이터가 아니라 주입을 통해 값을 가져올 수 있습니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.annotation.JacksonInject;

import java.util.StringJoiner;

public class BeanWithJacksonInject {
    @JacksonInject
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", BeanWithJacksonInject.class.getSimpleName() + "[", "]")
                .add("id=" + id)
                .add("name='" + name + "'")
                .toString();
    }
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class BeanWithJacksonInjectTests {
    @Test
    @DisplayName("JsonInjection필드에 값 주입하기")
    public void givenJsonWithoutSomeFieldProvided_whenInjectAndDeserialize_thenCorrect() throws JsonProcessingException {
        // given
        String json = "{\"name\":\"name\"}";

        // when
        InjectableValues injectableValues = new InjectableValues.Std().addValue(int.class, 1);
        BeanWithJacksonInject bean = new ObjectMapper().reader(injectableValues).forType(BeanWithJacksonInject.class).readValue(json);

        // then
        assertAll(
                () -> assertEquals(1, bean.getId()),
                () -> assertEquals("name", bean.getName())
        );

        // log
        System.out.println(json);
        System.out.println(bean);
    }
}

JUnit 테스트 결과

테스트를 통해 JSON에는 없는 값을 주입한 것을 확인할 수 있습니다.

@JsonAnySetter

Map을 속성으로 사용할 수있는 유연성을 제공합니다. 역직렬화시 JSON의 특성이 맵에 추가됩니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonGetter;

import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;

public class ExtendableBean {
    private String name;
    private Map<String, Object> properties;

    public ExtendableBean(String name) {
        this.name = name;
        this.properties = new HashMap<>();
    }

    protected ExtendableBean() {
        this.properties = new HashMap<>();
    }

    @JsonAnyGetter
    public Map<String, Object> getProperties() {
        return properties;
    }

    @JsonAnySetter
    public void add(String key, Object value) {
        properties.put(key, value);
    }

    @JsonGetter
    public String getUnitName() {
        return this.name;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", ExtendableBean.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("properties=" + properties)
                .toString();
    }
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ExtendableBeanTests {

    @Test
    @DisplayName("JsonAnySetter를 적용하여 역직렬화하기")
    public void givenJsonAnySetterAppliedAndJsonProvided_whenDeserialize_ExpectCorrect() throws JsonProcessingException {
        // given
        String json = "{\n" +
                "  \"id\": 1,\n" +
                "  \"name\": \"hydralisk\",\n" +
                "  \"damage\": \"10\",\n" +
                "  \"morphable\": true,\n" +
                "  \"hasHero\": true\n" +
                "}";

        // when
        ExtendableBean bean = new ObjectMapper().readValue(json, ExtendableBean.class);

        // then
        assertTrue(() -> bean.getProperties().containsKey("hasHero"));

        // log
        System.out.println(bean);
    }
}

JUnit 테스트 결과

직렬화 편에서 사용했던 예제에 추가하여 정확한 결과가 나오진 않았지만 맵 형태로 properties에 추가된 것을 확인할 수 있습니다.

@JsonSetter

@JsonProperty의 대안으로 메소드를 setter 메소드로 지정합니다. JSON 데이터를 읽어야하지만 대상 엔터티 클래스가 해당 데이터와 정확히 일치하지 않을 때 유용하게 사용할 수 있습니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.annotation.JsonSetter;

import java.util.StringJoiner;

public class BeanWithJsonSetter {
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    @JsonSetter
    public void setMyName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", BeanWithJsonSetter.class.getSimpleName() + "[", "]")
                .add("id=" + id)
                .add("name='" + name + "'")
                .toString();
    }
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;


public class BeanWithJsonSetterTests {
    @Test
    @DisplayName("JsonSetter를 이용하여 역직렬화하기")
    public void givenJsonSetterAppliedAndJsonProvided_whenDeserialize_thenCorrect() throws JsonProcessingException {
        // given
        String json = "{\n" +
                "  \"id\": 1,\n" +
                "  \"myName\": \"Jungmin Lee\"\n" +
                "}";

        // when
        BeanWithJsonSetter bean = new ObjectMapper().readValue(json, BeanWithJsonSetter.class);

        // then
        assertEquals("Jungmin Lee", bean.getName());

        // log
        System.out.println(json);
        System.out.println(bean);
    }
}

JUnit 테스트 결과

@JsonDeserialize

Custom Deserializer를 사용할 수 있게 해줍니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.lcalmsky.jackson.domain.deserializer.CustomDateDeserializer;

import java.util.Date;
import java.util.StringJoiner;

public class BeanWithDeserializer {
    private String name;
    @JsonDeserialize(using = CustomDateDeserializer.class)
    private Date date;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", BeanWithDeserializer.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("date=" + date)
                .toString();
    }
}
package io.lcalmsky.jackson.domain.deserializer;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class CustomDateDeserializer
        extends StdDeserializer<Date> {

    private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public CustomDateDeserializer() {
        this(null);
    }

    public CustomDateDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Date deserialize(JsonParser jsonparser, DeserializationContext context) throws IOException {

        String date = jsonparser.getText();
        try {
            return formatter.parse(date);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.text.SimpleDateFormat;

import static org.junit.jupiter.api.Assertions.assertEquals;


public class BeanWithDeserializerTests {
    @Test
    @DisplayName("Deserializer를 지정하여 날짜형식을 역직렬화함")
    public void givenBeanWithDeserializerProvided_whenDeserialize_thenCorrect() throws JsonProcessingException {
        // given
        String json = "{\"name\":\"today\",\"date\":\"2019-11-19 15:45:00\"}";

        // when
        BeanWithDeserializer bean = new ObjectMapper().readValue(json, BeanWithDeserializer.class);

        // then
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        assertEquals("2019-11-19 15:45:00", format.format(bean.getDate()));

        // log
        System.out.println(bean);
    }
}

JUnit 테스트 결과

@JsonAlias

역 직렬화 중에 속성에 대한 하나 이상의 대체 이름을 정의합니다.

package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BeanWithAlias {
    @JsonAlias({"first_name", "fName"})
    private String firstName;
    private String lastName;
}
package io.lcalmsky.jackson.domain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class BeanWithAliasTests {
    @Test
    @DisplayName("@JsonAlias에 추가한 필드명으로 JSON을 구성하여 역직렬화하여도 같은 결과를 획득")
    public void givenJsonAliasProvided_whenDeserialize_thenCorrect() throws JsonProcessingException {
        // given
        String json1 = "{\"firstName\":\"Jungmin\",\"lastName\":\"Lee\"}";
        String json2 = "{\"first_name\":\"Jungmin\",\"lastName\":\"Lee\"}";
        String json3 = "{\"fName\":\"Jungmin\",\"lastName\":\"Lee\"}";

        // when
        ObjectMapper objectMapper = new ObjectMapper();
        BeanWithAlias bean1 = objectMapper.readValue(json1, BeanWithAlias.class);
        BeanWithAlias bean2 = objectMapper.readValue(json2, BeanWithAlias.class);
        BeanWithAlias bean3 = objectMapper.readValue(json3, BeanWithAlias.class);

        // then
        assertAll(
                () -> assertEquals(bean1.getFirstName(), bean2.getFirstName()),
                () -> assertEquals(bean2.getFirstName(), bean3.getFirstName())
        );

        // log
        System.out.println(bean1);
        System.out.println(bean2);
        System.out.println(bean3);
    }
}

JUnit 테스트 실행 결과

firstName 필드에 상응하는 key를 각각 다르게 지정하였지만 모두 정확하게 매핑된 것을 확인할 수 있습니다.