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);
}
}
@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);
}
}
테스트를 통해 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);
}
}
직렬화 편에서 사용했던 예제에 추가하여 정확한 결과가 나오진 않았지만 맵 형태로 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);
}
}
@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);
}
}
@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);
}
}
firstName 필드에 상응하는 key를 각각 다르게 지정하였지만 모두 정확하게 매핑된 것을 확인할 수 있습니다.