kotlin을 사용하면 data class나 lazy loading 문제들 때문에, Json 직렬화, 역직렬화를 원활하게 하기 위헤 부가적인 설정들이 필요하다. 나는 커스텀하게 쓰는 설정들이 있으니, 여기서 공유해보고자 한다.
//json
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.13.3")
@Configuration
class JacksonConfig {
class MyPropertyNamingStrategy : PropertyNamingStrategy() {
override fun nameForField(config: MapperConfig<*>?, field: AnnotatedField, defaultName: String?): String {
return field.name
}
override fun nameForGetterMethod(
config: MapperConfig<*>?,
method: AnnotatedMethod,
defaultName: String,
): String {
return convert(method, defaultName)
}
override fun nameForSetterMethod(
config: MapperConfig<*>?,
method: AnnotatedMethod,
defaultName: String,
): String {
return convert(method, defaultName)
}
private fun convert(method: AnnotatedMethod, defaultName: String): String {
val clazz = method.declaringClass
val flds = FieldUtils.getAllFieldsList(clazz)
for (fld in flds) {
if (fld.name.equals(defaultName, ignoreCase = true)) {
return fld.name
}
}
return defaultName
}
}
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
val javaTimeModule = JavaTimeModule()
//LocalDateTime 원하는 커스텀 포맷으로 직/역직렬화
javaTimeModule.addSerializer(LocalDateTime::class, CustomLocalDateTimeSerializer())
javaTimeModule.addDeserializer(LocalDateTime::class, CustomLocalDateTimeDeSerializer())
objectMapper.registerModule(javaTimeModule)
//objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//아직 불러오지 않은 엔티티에 대해 null값을 내려주는 모듈이다. lazy loading
objectMapper.registerModule(Hibernate5Module())
// 모르는 property에 대해 무시하고 넘어간다.
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper.registerModule(
KotlinModule.Builder()
.withReflectionCacheSize(512)
.configure(KotlinFeature.NullToEmptyCollection, false)
.configure(KotlinFeature.NullToEmptyMap, false)
.configure(KotlinFeature.NullIsSameAsDefault, false)
.configure(KotlinFeature.SingletonSupport, false)
.configure(KotlinFeature.StrictNullChecks, false)
.build())
// 시간 관련 객체(LocalDateTime, java.util.Date)를 직렬화 할 때 timestamp 숫자값이 아닌 포맷팅 문자열로 한다.
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// Set PropertyNamingStrategy to use original field names
objectMapper.propertyNamingStrategy = MyPropertyNamingStrategy()
return objectMapper
}
class CustomLocalDateTimeSerializer(): JsonSerializer<LocalDateTime>() {
override fun serialize(value: LocalDateTime, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(formatter.format(value))
}
}
class CustomLocalDateTimeDeSerializer(): JsonDeserializer<LocalDateTime>(){
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
return LocalDateTime.parse(p.text, formatter)
}
}
companion object {
private val dateTimeFormat = "yyyy-MM-dd HH:mm:ss"
private val formatter = DateTimeFormatter.ofPattern(dateTimeFormat, Locale.KOREA)
}
}
예를 들어 다음과 같은, enum class 가 존재하고, json으로 직렬화하고 싶을 때는, value 값으로, 자바 객체로 역직렬화하고 싶을 때는, value든 enum의 name이든 다 같이 변환하고 싶을 때는 요렇게 작성하면 된다.
enum class PcdPayType(
@field:JsonValue
val value:String
){
CARD("card"),
TRANSFER("transfer"),
;
companion object {
@JvmStatic
@JsonCreator
fun fromJson(key:String): PcdPayType {
return values().firstOrNull { it.value == key } ?: valueOf(key)
}
}
}
@Test
fun objectMapperTest() {
val mapper = JacksonConfig().objectMapper()
val jsonData = """
{"PCD_PAY_TYPE":"card","PCD_PAY_WORK":"AUTH"}
""".trimIndent()
val dto = mapper.readValue(jsonData, PayDto::class.java)
println(dto)
}
예전에 응답 DTO로 공통 DTO를 작성했다가, API EndPoint 마다 field name을 다르게 내려주라고 해서 급히 바꾼 기억이 있다. 여전히 공통 DTO는 유지하되 동적으로 key name만 바꾸고 싶다면 다음과 같이 하면 된다. 그 때 작성한 코드 샘플(JAVA)이다.
@NoArgsConstructor
@AllArgsConstructor
public class ResultDto<T> {
@Getter
@Setter
private String resultCode;
@Getter
@Setter
private String resultMsg;
@Getter
@Setter
private Long totalCount;
//private T data;
private Map<String, T> datas;
@JsonAnyGetter
public Map<String, T> getDatas() {
return this.datas;
}
@JsonAnySetter
public void setDatas(Map<String, T> datas) {
this.datas = datas;
}
}
var responseData = //data를 받아온다.
new ResultDto(HttpStatus.OK.toString(), "api description", 1L,
Collections.singletonMap("붙이고 싶은 이름", responseData));
응답 dto 안의 datas 안의 것도 동적으로 바꾸고 싶다면..
public class CustomPageImpl<T> {
private Map<String, List<T>> datas;
@Getter
@Setter
private String first;
@Getter
@Setter
private String last;
@Getter
@Setter
private Integer pageNumber;
@Getter
@Setter
private Integer elementsPerPage;
@JsonAnyGetter
//@JsonProperty(index = -3)
public Map<String, List<T>> getDatas() {
return this.datas;
}
@JsonAnySetter
public void setDatas(Map<String, List<T>> datas, String contentName) {
this.datas = datas;
}
public CustomPageImpl2(Map<String, List<T>> datas, Boolean first, Boolean last, Integer pageNumber, Integer elementsPerPage) {
this.datas = datas;
this.first = String.valueOf(first);
this.last = String.valueOf(last);
this.pageNumber = pageNumber;
this.elementsPerPage = elementsPerPage;
}
}
//사용
CustomPageImpl2<타입> items = new CustomPageImpl<>(
Collections.singletonMap("붙이고싶은 이름", allData.getContent()),
allData.isFirst(),
allData.isLast(),
allData.getNumber(),
allData.getNumberOfElements()
);
return new ResultDto(HttpStatus.OK.toString(), "find all datas",
Long.valueOf(allData.getTotalElements()),
Collections.singletonMap("allDatas_items_with_page", items));
https://www.baeldung.com/kotlin/jackson-kotlin
https://kwonnam.pe.kr/wiki/java/jackson
https://stackoverflow.com/questions/26744885/jackson-objectmapper-upper-lower-case-issues
https://github.com/cheese10yun/blog-sample/blob/master/jackson/README.md