Jackson에서 사용하는 Annotation에 대해 정리한다. Baeldung (Jackson Annotation Example)에 정리된 글을 참고하였다.
2. Jackson Deserialization Annotations
2.1 @JsonCreator
@JsonCreator는 Json객체를 deserialize 하여 객체 mapping 시 사용할 생성자를 지정할 수 있도록 도와준다. 이때 주의할 점은 @JsonCreator는 반드시 @JsonProperty와 함께 사용되어야 한다. @JsonProperty는 @JsonCreator의 파라미터 필드에 정의되어 Mapping을 위한 property값을 정의한다.
파라미터의 이름이 property 명과 일치한다면 @JsonProperty 애너테이션을 사용하지 않고 처리해도되지 않을까?라는 의문이 생길 수 있다. 하지만 함수가 컴파일되면 파라미터를 'var0', 'var1'과 같이 임의의 이름으로 바뀌게된다. 즉, Jackson이 파싱 한 json데이터를 deserialize 하기 위해 key value값을 얻어와도 이름을 통해서는 어떤 필드에 값을 적용해야 되는지 알 수 없다. 그렇기에 각 파라미터마다 @JsonProperty 애노테이션을 붙여야만 property 이름에 맞는 필드를 찾고 Reflection API로 setting 해줄수 있다.
public class BeanWithCreator {
public int id;
public String name;
@JsonCreator
public BeanWithCreator(@JsonProperty("id") int id, @JsonProperty("theName") String name) {
this.id = id;
this.name = name;
}
}
@Test
public void testJsonCreator() throws IOException {
String json = "{\"id\":1,\"theName\":\"My bean\"}";
BeanWithCreator bean = new ObjectMapper().readerFor(BeanWithCreator.class).readValue(json);
assertEquals(1, bean.getId());
assertEquals("My bean", bean.getName());
}
Jackson이 deserialize 과정에서 별도의 설정이 없을 때 처리하는 동작은 다음과 같다.
1. 기본 생성자를 이용해서 생성
2. 필드가 public이면 직접 할당
3. 필드가 private이면 setter 사용
위의 과정 중 @JsonCreator는 (기본 생성자) + (setter) 조합을 대체한다. 이 어노테이션이 생성자나 팩토리 메서드 위에 선언되면 Jackson은 해당 함수를 통해 객체를 생성하고 필드를 생성과 동시에 채운다. 이러한 방식은 setter 함수 없이 필드 주입을 시킬 수 있다는 것이 특징이다. Jackson을 통해 immutable 한 객체(setter 함수가 없으므로)를 얻기 위한 하나의 방법으로 사용할 수 있다.
2.2 @JacksonInject
@JacksonInject 애노테이션은 선언된 필드가 Json 데이터가 아닌 inject 될 것임을 알려준다.
public class BeanWithInject {
@JacksonInject
private int id;
private String name;
// getter setter
}
@Test
public void testJacksonInject() throws IOException {
String json = "{\"name\":\"My bean\"}";
InjectableValues inject = new InjectableValues.Std()
.addValue(int.class, 1); // id값으로 '1' 주입
BeanWithInject bean = new ObjectMapper().reader(inject)
.forType(BeanWithInject.class)
.readValue(json);
System.out.println(bean.getId());
System.out.println(bean.getName());
}
/** @JacksonInject 적용 전 */
> 0
> My bean
/** @JacksonInject 적용 후 */
> 1
> My bean
2.3 @JsonAnySetter
@JsonAnyGetter는 Map을 properties로 serialize 했다면, @JsonAnySetter는 properties를 Map으로 deserialize 한다. 아래의 테스트는 문자열 json에 대한 deserialization이다. 만약 필드 이름이 "name"과 동일하지 않은 다른 값이었다면 이 값 또한 Map에 key value쌍으로 들어간다.
public class ExtendableBean {
public String name;
private Map<String, String> properties = new HashMap<>();
@JsonAnySetter
public void add(String key, String value) {
properties.put(key, value);
}
public Map<String, String> getProperties() {
return properties;
}
}
@Test
public void testJsonAnySetter() throws IOException {
String json = "{\"name\":\"My bean\",\"attr2\":\"val2\",\"attr1\":\"val1\"}";
ExtendableBean bean = new ObjectMapper()
.readerFor(ExtendableBean.class)
.readValue(json);
System.out.println(bean.getProperties());
// {attr2=val2, attr1=val1}
}
2.4 @JsonSetter
method에 대하여 value로 정한 필드의 setter로 지정한다. 아래의 예시는 setTheName() 메서드를 name 필드의 setter로 지정한 모습이다. 즉, deserialization 과정에서 name필드는 setTheName() 메서드를 통해 세팅된다.
public class MyBean {
public int id;
private String name;
@JsonSetter("name")
public void setTheName(String name) {
this.name = name;
}
}
2.5 @JsonDeserialize
unmarshalling 과정에 custom deserializer를 사용하도록 지정한다. @JsonSerialize와 반대의 역할을 하는 애노테이션이며 이전 게시물에 설명한 serialization과 marshalling의 차이처럼 deserialization과 unmarshalling의 차이를 이해하면 되겠다.
아래의 예시에서는 CustomDataDeserializer를 이용하여 역직렬화하도록 지정해주었다.
public class EventWithSerializer {
private String name;
@JsonSerialize(using = CustomDateSerializer.class)
@JsonDeserialize(using = CustomDateDeserializer.class)
private Date eventDate;
public EventWithSerializer() {
}
public EventWithSerializer(String name, Date eventDate) {
this.name = name;
this.eventDate = eventDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getEventDate() {
return eventDate;
}
public void setEventDate(Date eventDate) {
this.eventDate = eventDate;
}
}
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<?> t) {
super(t);
}
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
String date = jsonParser.getText();
try {
return formatter.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void testJsonDeserialize() throws IOException {
String json = "{\"name\":\"party\",\"eventDate\":\"2020-09-12 01:12:34\"}";
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
EventWithSerializer event = new ObjectMapper()
.readerFor(EventWithSerializer.class)
.readValue(json);
assertEquals("2020-09-12 01:12:34", df.format(event.getEventDate()));
}
2.6 @JsonAlias
@JsonAlias는 필드나 파라미터 위에 선언되어 하나 혹은 그 이상의 대체 property 이름으로 deserialize 되도록 해준다. 아래의 예제는 필드 firstName에 대하여 이름이 fName 혹은 f_name인 property가 나온다면 deserialize 해주도록 지정한다. 두 개 이상의 properties가 모두 등장한다면 (순차적으로 parsing 하는 것 같다) 가장 늦게 선언한 property의 값으로 setting 하는 것을 확인할 수 있다.
public class AliasBean {
@JsonAlias({ "fName", "f_name" })
private String firstName;
private String lastName;
// getter, setter
}
/**
* 하나의 property만 match (fName)
*/
@Test
public void testJsonAlias_matchOne() throws IOException {
String json = "{\"fName\": \"John\", \"lastName\": \"Green\"}";
AliasBean aliasBean = new ObjectMapper()
.readerFor(AliasBean.class)
.readValue(json);
System.out.println(aliasBean.getFirstName()); // John
}
/**
* 두개의 properties match (fName, f_name)
*/
@Test
public void testJsonAlias_matchesTwo() throws IOException {
String json = "{\"fName\": \"John\", \"f_name\": \"Joe\", \"lastName\": \"Green\"}";
AliasBean aliasBean = new ObjectMapper()
.readerFor(AliasBean.class)
.readValue(json);
System.out.println(aliasBean.getFirstName()); // Joe
}