[Java] Jackson ObjectMapper Serialization
```kt
val MY_OBJECT_MAPPER = jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false)
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
.setSerializationInclusion(Include.NON_NULL)
//.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
+ LocalDateTime Serialize/DeSerialize 시 기본적으로 "2021-12-07T21:53:04.882" 같이 동작하도록 하려면
val module = JavaTimeModule().apply { addSerializer(LocalDateTimeSerializer(DateTimeFormatter.ISO_DATE_TIME)) }
MY_OBJECT_MAPPER.registerModule(module) 도 추가
Spring에서 new RestTemplate(), WebClient() 시에 사용하는 jackson은 ObjectMapper()로 생성하고 끝이 아니라
MappingJackson2HttpMessageConverter() 로 생성하고 있음.
== Jackson2ObjectMapperBuilder.json().build() 임.
```
Spring Boot가 어떤 기본 설정 사용하는지는 `` JacksonAutoConfiguration, Jackson2ObjectMapperBuilder`` 참조
https://www.baeldung.com/spring-boot-customize-jackson-objectmapper
https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations
https://www.tutorialspoint.com/jackson_annotations/jackson_annotations_jsonanysetter.htm
ObjectMapper 는 생성 비용이 꽤 비싸기 때문에 멤버변수 / bean / static으로 처리
- ObjectMapper는 Thread Safe하기 때문에 굳이 매번 생성해서 따로 쓸 필요가 없음.
- 그리고 생성 비용이 비싼 편이라 매번 생성하도록 하는 경우 138배까지 성능 차이가 발생할 수 있음. (마스크 대란)
- 그래서 reuse를 권장하고 있음.
- case Bean: 전역적으로 ObjectMapper를 설정해야 하는 경우 Bean으로 만들어서 DI 받아 쓰는 것이 좋다.
- case (static) Field: 해당 클래스에서 사용하는 ObjectMapper에 별도 설정이 필요한 경우.
- static은 의미에 따라 붙이기도, 안붙이기도. 어차피 bean이라서...
POJO <> json String
```java
import com.fasterxml.jackson.databind.ObjectMapper;
// POJO -> JSON String
String jsonStr = objectMapper.writeValueAsString(new BillingKey("example_key"))
//JSON String -> POJO
BillingKey pojo = objectMapper.readValue(jsonStr, BillingKey.class);
```
*** readValue는 json String으로부터. convertValue는 Object로부터.
Map <> POJO
```java
Map<String, String> salesRequestJson = ...;
Payment payment = objectMapper.convertValue(salesRequestJson, Payment.class);
private static final TypeReference<Map<String, String>> MAP_TYPE_REFERENCE =
new TypeReference<Map<String, String>>() {};
Map<String, String> requestMap = objectMapper.convertValue(request, MAP_TYPE_REFERENCE);
```
jackson ObjectMapper의 json <> data class 매핑 룰
json -> data class 매핑 룰
`` @AllArgsConstructor``로 하면 생성자 통해 매핑되는데, 직접 정의한 all args constructor로 하면 400에러가 발생한다?!
=> Bytecode 확인해보니 `` @AllArgsConstructor``를 사용하면 `` @ConstructorProperties``가 자동으로 붙는다
그래서 확인해봤다.
```java
public class TestCls {
@Setter @Getter
private String pa;
@Getter
private String pa2;
@ConstructorProperties({"pa"})
public TestCls(String pa) {
this.pa = "constructor init";
this.pa2 = "constructor init";
}
```
```
요청 파라미터 : { "pa"="param init", "pa2"="param init" }
실행 결과 : pa=constructor init, pa2=param init
```
- @ConstructorProperties가 있다면, 이게 붙은 생성자 먼저 실행하여 초기화한다.
- 1에서 초기화 되지 않은 필드 중 식별 가능한 필드를 초기화한다.
- jackson은 기본적으로 Getter/Setter에서 get/set prefix 잘라내고 맨앞 소문자로 만드는 것으로 필드를 식별한다.
- pa2는 ConstructorProperties에는 없으나 @Getter가 붙어있으므로 param init으로 초기화 된 것
- boolean 변수의 경우 get 대신 is를 잘라낸다. (이를 고려한 Naming Convention 참조)
- 식별한 필드 목록에 대해서, Setter 있으면 setter 호출해서 넣어주고 없다면 reflection으로 넣어준다.
- jackson은 기본적으로 Getter/Setter에서 get/set prefix 잘라내고 맨앞 소문자로 만드는 것으로 필드를 식별한다.
*** ConstructorProperties에도 포함되지 않으며 Getter/Setter도 붙어있지 않다면 그 필드는 식별되지 않아 초기화 X
*** @ConstructorProperties는 하나만 존재해야 한다. 여러개 있으면 conflict error 발생
*** json에 포함되어 있지 않은 필드는 당연히 setter 거치지 않고 null이 된다.
data class -> json Serialization 룰
Getter 보고 data class에서 필드 식별 및 가져와서 json으로 만든다.
ConstructorProperties와 Setter는 식별에 관여 X
그래서 Getter가 안붙어 있는 경우, No HttpMessageConverter for xxx.xxx...Request 예외가 발생함.
Annotation
이름 변환 JSON <> POJO <> DB
```java
@JsonProperty("error_code")
private String errorCode;
```
- 물론 DB의 경우 `` as``를 쓰는 방법도 있지만... as를 쓰는 경우 sql마다 다 as를 적어줘야하고, 혹시라도 이름이 변경되면 이들을 찾아다니면서 다 변경해주어야 해서 좀 더 번거롭다.
- 하지만 as를 쓰면 WHERE 문에서 as 뒤의 변수명을 사용할 수 있어 이게 더 나을 수도 있음.
이름 변환 2
deserialize 시에만 이름을 바꿔서 받고 싶다면?
jackson은 getter/setter에서 get/set prefix를 떼어내 필드를 식별하고 파라미터와 매핑하므로
```java
private String originName;
public void setParamName(String paramName) { originName = paramName; }
```
또는
```java
private String originName;
@JsonSetter("paramName")
public void setOriginName(String originName) { originName = originName; }
@JsonGetter("originName") // 이걸 안하면 serialize 시에도 paramName이 되어 버린다.
public String getOriginName() { return originName; }
+++ @AllArgsConstructor는 없어야 하고 @NoArgsConstructor 는 있어야 함. 이렇게 안하면 no suitable HttpMessageConverter ... Error남.
```
- 왜 @JsonGetter("originName")까지 해줘야 되는지는 다음을 보면 나와있음. (@JsonSetter도 마찬가지.)
- https://github.com/FasterXML/jackson-databind/issues/1519
- 그래서 결국 한쪽만 필요하더라도 set/get 양쪽 다 붙여줘야 하기 때문에... @JsonGetter/@JsonSetter 대신 @JsonProperty를 get/set에 붙여주어도 똑같다.
- 근데 이렇게 까지 해야하는 상황이면 보통은 받는 쪽에서 @JsonAlias 써서 처리하는게 나을 때가 많다
serialize 시에만 이름을 다르게 바꿔서 json으로 만들고 싶다면?
jackson은 getter에서 get prefix를 떼어내 식별한 필드를 json으로 구성하므로
```java
private String originName;
public String getNewName() { return originName; }
```
또는
```java
private String originName;
@JsonGetter("newName")
public String getOriginName() { return originName; }
@JsonSetter("originName") // 이걸 안하면 못받는다. 이유는 위 설명 참조.
public void setOriginName(String originName) { originName = originName; }
+++ @AllArgsConstructor는 없어야 하고 @NoArgsConstructor 는 있어야 함.
+++ 이렇게 안하면 no suitable HttpMessageConverter ... Error 발생
```
클라이언트가 보내주는 JSON data 중 특정 key에 대해서는 Mapping을 제외
클라이언트가 임의로 이런 속성을 만들어서 보내게 되었을 때 이상한 값이 들어가는 것 방지,
또는 로직 상 클라이언트에서 이 key:value를 보내지 않을 때.
```java
@JsonIgnore
private String method;
```
요청 받는 Dto와 응답 내려주는 Dto를 구분하는 경우 이런 식으로 아예 Ignore해버려도 상관 없지만,
하나의 Dto로 요청도 받고 응답도 받는 경우 요청에서는 무시하고 싶은데 응답에서는 JSON으로 내려주고 싶을 때
```java
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String salesTime;
```
Null인 필드에 대해서는 key-value쌍을 만들지 않도록 함
www.baeldung.com/jackson-ignore-null-fields
변수명 snake_case <-> camelCase 변환 자동으로 처리하기
[Java] Jackson 변수명 snake_case <-> camelCase 변환
Object 필드에 정의되지 않은 key-value 받아오면서 UnrecognizedPropertyException 발생 시
https://www.baeldung.com/jackson-deserialize-json-unknown-properties
기타
[Java] Enum to Json / Enum to Object
https://docs.spring.io/spring-android/docs/current/reference/html/rest-template.html
https://www.baeldung.com/spring-httpmessageconverter-rest
JSON string이 아니라, byte로 serialize하고 싶다면?
- Serializable 인터페이스 구현.
- 포함하고 싶지 않은 필드는 `` transient`` 키워드를 붙여준다. (Key=null 로 처리됨)
Jackson 없이 간단하게 변환하고 싶은 경우?
```java
JSONObject json = new JSONObject();
json.put("a", a);
json.toString(); // <-- jsonstr
```
'Java Stack > Java' 카테고리의 다른 글
[Effective Java] 2장 객체 생성과 파괴 (0) | 2019.12.02 |
---|---|
[Java] LocalDateTime : 날짜 시간 처리 관련 (0) | 2019.07.10 |
[Java] Enum (0) | 2019.06.06 |
[Java] Jackson ObjectMapper Serialization (0) | 2019.05.15 |
[Java] Stream API 노트 (0) | 2017.03.09 |
[Java] lambda 기본 개념 (0) | 2017.02.07 |
댓글
이 글 공유하기
다른 글
-
[Java] LocalDateTime : 날짜 시간 처리 관련
[Java] LocalDateTime : 날짜 시간 처리 관련
2019.07.10 -
[Java] Enum
[Java] Enum
2019.06.06 -
[Java] Stream API 노트
[Java] Stream API 노트
2017.03.09 -
[Java] lambda 기본 개념
[Java] lambda 기본 개념
2017.02.07