Kotlin ObjectMapper(Jackson) 설정

공부는 혼자하는 거·2023년 5월 25일
0

Spring Tip

목록 보기
39/52

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")

ObjectMapper Config

@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)

    }


}

소소한 Tip

enum 조작

예를 들어 다음과 같은, 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 field key 값 동적 변경

예전에 응답 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

profile
시간대비효율

0개의 댓글