Post

(Java) Jackson ObjectMapper Serialization

ObjectMapper Configuration

SpringBoot가 생성 및 제공하는 ObjectMapper Bean / configuration

  • Spring Boot가 default ObjectMapper Bean 생성 할 때 관여하는 클래스는 JacksonAutoConfiguration, Jackson2ObjectMapperBuilder이므로, 이 두 클래스를 참고하면 기본 설정을 알아낼 수 있다.
    • 역할
    • org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration -> application.yml의 spring.jackson… 설정 적용.
    • org.springframework.http.converter.json.Jackson2ObjectMapperBuilder -> 의존성에 jsr310 등 모듈이 있으면 자동으로 Module 등록
  • SpringBoot 제공 ObjectMapper에 대한 customizing은https://www.baeldung.com/spring-boot-customize-jackson-objectmapper참고

SpringBoot 제공 Bean 말고 직접 정의해서 사용하기

  • static class나 library 같이 SpringContext 바깥에서 ObjectMapper를 사용해야 하는 경우, 직접 정의해서 사용해야 함.
  • 한 시스템 내에서 ObjectMapper의 기본 설정은 통일하는 것이 좋기 때문에 SpringBoot 제공 기본 설정과 유사하게 설정
1
2
3
4
/** JacksonAutoConfiguration, Jackson2ObjectMapperBuilder를 둘 다 사용하는 경우
  * JacksonAutoConfiguration가 Bean이기 때문에, 
  * Spring Context 바깥에서 두 클래스를 모두 사용해서 ObjectMapper 생성하는 것은 불가능하다.
  */
1
2
3
4
5
6
7
8
9
10
11
/** Jackson2ObjectMapperBuilder만 사용하는 경우
  * application.yml에서 세팅하는 spring.jackson... -> 가 적용되지 않음. 직접 설정 필요.
  */
val builderObjectMapper = Jackson2ObjectMapperBuilder.json()
    .postConfigurer {
        it.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        it.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false)
        it.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
    }
    .serializationInclusion(JsonInclude.Include.NON_NULL)
    .build<ObjectMapper>()
1
2
3
4
5
6
7
8
9
10
11
12
/** Jackson2ObjectMapperBuilder 조차 사용 할 수 없는 경우
  * application.yml에서 세팅하는 spring.jackson... -> 가 적용되지 않음. 직접 설정 필요.
  * Jackson2ObjectMapperBuilder에서 넣어주는 기본 jsr310, KotlinModule 등이 적용되지 않음. 직접 설정 필요.
  */
val MY_OBJECT_MAPPER = jacksonObjectMapper()  // kotlin module
  .registerModule(JavaTimeModule());  // !!2.12.0 부터 LocalDate 계열 (de)serialize를 위해 반드시 추가 필요!!
  .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)

서로 다른 두 ObjectMapper에 설정이 동일하게 적용 되었는지를 확인하려면 크게 Features와 Modules를 확인해보면 된다.

Features는 mask로 표현되기 때문에 정수다.

static 변수로 놓고 nested bean으로 초기화 하는 방법도 있다. 참고

기타 customizing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JavaTimeModule을 설정하지 않으면 LocalDate/Time 변환 시
{"year":2023,"month":"MAY","monthValue":5,...} 같은 포맷으로 deserialize 되거나 2.10.0 이상에서는 아예 에러가 발생한다.
// 별도 설정 없이 JavaTimeModule 추가 시
"2023-05-15T10:05:11.354" 포맷으로 변환된다.

// + LocalDateTime Serialize/DeSerialize 시 기본 (de)serialize 포맷을 변경하려면 JavaTimeModule에 설정
val MY_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val javaTimeModule = JavaTimeModule().apply { 
  addSerializer(LocalDateTimeSerializer(MY_DATE_TIME_FORMATTER))
}
MY_OBJECT_MAPPER.registerModule(javaTimeModule)

// 특정 ObjectMapper에 SNAKE_CASE 변환을 넣고 싶다면
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

ObjectMapper 직접 생성 할 때

static 전역 object VS createMethod
1
2
3
4
5
6
7
8
public class JacksonUtil {
    public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
    /*** VS ***/
    public static ObjectMapper createObjectMapper() {
        return new ObjectMapper()
            .configure(기본 설정들...);
    }
}
  • createMethod 방식이 더 나은 이유는, static final이라고 해도 외부에서 OBJECT_MAPPER에 대한 설정이 가능하기 때문이다.
  • 클라이언트 코드가 OBJECT_MAPPER.configure 해버리면 전역 객체에 설정이 적용되어 시스템 전체에 영향이 간다.
ObjectMapper 는 생성 비용이 꽤 비싸기 때문에 멤버변수 / bean / static으로 처리
  • ObjectMapper는 Thread Safe하기 때문에 굳이 매번 생성해서 따로 쓸 필요가 없음.
  • 그리고 생성 비용이 비싼 편이라 매번 생성하도록 하는 경우 138배까지 성능 차이가 발생할 수 있음. (마스크 대란)
  • 그래서 reuse를 권장하고 있음.
  • case Bean: 전역적으로 ObjectMapper를 설정해야 하는 경우 Bean으로 만들어서 DI 받아 쓰는 것이 좋다.
  • case (static) Field: 해당 클래스에서 사용하는 ObjectMapper에 별도 설정이 필요한 경우.
    • static은 의미에 따라 붙이기도, 안붙이기도. 어차피 bean이라서…
POJO <> json String
1
2
3
4
5
6
7
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
1
2
3
4
5
6
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가 자동으로 붙는다

그래서 확인해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
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";
    }
}
1
2
요청 파라미터 : { "pa"="param init", "pa2"="param init" }
실행 결과 : pa=constructor init, pa2=param init
  1. @ConstructorProperties가 있다면, 이게 붙은 생성자 먼저 실행하여 초기화한다.
  2. 1에서 초기화 되지 않은 필드 중 식별 가능한 필드 를 초기화한다.

    • jackson은 기본적으로 Getter/Setter에서 get/set prefix 잘라내고 맨앞 소문자로 만드는 것으로 필드를 식별한다.
      • pa2는 ConstructorProperties에는 없으나 @Getter가 붙어있으므로 param init으로 초기화 된 것
      • boolean 변수의 경우 get 대신 is를 잘라낸다. (이를 고려한Naming Convention 참조)
    • 식별한 필드 목록에 대해서, Setter 있으면 setter 호출해서 넣어주고 없다면 reflection으로 넣어준다.

*** ConstructorProperties에도 포함되지 않으며 Getter/Setter도 붙어있지 않다면 그 필드는 식별되지 않아 초기화 X *** @ConstructorProperties는 하나만 존재해야 한다. 여러개 있으면 conflict error 발생 *** json에 포함되어 있지 않은 필드는 당연히 setter 거치지 않고 null이 된다.

참고 ) https://blog.benelog.net/jackson-with-constructor.html

data class -> json Serialization 룰

Getter 보고 data class에서 필드 식별 및 가져와서 json으로 만든다. ConstructorProperties와 Setter는 식별에 관여 X

그래서 Getter가 안붙어 있는 경우,No HttpMessageConverter for xxx.xxx…Request 예외가 발생함.

Annotation

Jackson Annotation 모음

https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations

https://www.tutorialspoint.com/jackson_annotations/jackson_annotations_jsonanysetter.htm

이름 변환 JSON <> POJO

1
2
@JsonProperty("error_code")
private String errorCode;

kotlin의 경우, isAbc 같이 is가 붙어있는 변수에는 @get:JsonProperty 사용해야 한다.

1
2
3
// @JsonProperty("is_space") 동작하지 않는다.
@get:JsonProperty("is_space")
val isSpace: Int;

이름 변환 2

deserialize 시에만 이름을 바꿔서 받고 싶다면?

jackson은 getter/setter에서 get/set prefix를 떼어내 필드를 식별하고 파라미터와 매핑하므로

1
2
private String originName;
public void setParamName(String paramName) { originName = paramName; }

또는

1
2
3
4
5
6
7
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으로 구성하므로

1
2
private String originName;
public String getNewName() { return originName; }

또는

1
2
3
4
5
6
7
8
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를 보내지 않을 때.

1
2
@JsonIgnore
private String method;

요청 받는 Dto와 응답 내려주는 Dto를 구분하는 경우 이런 식으로 아예 Ignore해버려도 상관 없지만,

하나의 Dto로 요청도 받고 응답도 받는 경우 요청에서는 무시하고 싶은데 응답에서는 JSON으로 내려주고 싶을 때
1
2
@JsonProperty(access = JsonProperty.Access.READ\_ONLY)
private String salesTime;
사용처에 따라 어떤 필드는 노출, 어떤 필드는 노출X 하고 싶을 때 (JsonView)

https://javafactory.tistory.com/1518

DEFAULT_VIEW_INCLUSION 옵션과 관련 있음.

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

xIdxid 로 변환되나요?

기타

[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 없이 간단하게 변환하고 싶은 경우?

1
2
3
JSONObject json = new JSONObject();
json.put("a", a);
json.toString();  // <-- jsonstr

JsonSerializer

1
2
3
4
5
6
7
8
9
10
11
12
class MySerializer : JsonSerializer<String?>() {  
    override fun serialize(value: String?, gen: JsonGenerator?, serializers: SerializerProvider?) {  
        runCatching {  
            value?.let { gen?.writeString(it.lowercase()) }  
        }.onFailure {  
            gen?.writeNull()  
        }  
    }  
}

@JsonSerialize(using = DecryptionSerializer::class)
val myField: String?

gen에 반드시 write가 발생해야만 한다. 오류 케이스에서 아무것도 안쓰고 메서드 종료한다면, jackson field mapping이 한 칸씩 밀려 에러난다.

1
JsonMappingException: Can not write a field name, expecting a value

위 예제에서는 writeNull() 처리 했다.

This post is licensed under CC BY 4.0 by the author.