라이브러리

json 다형성 직렬 및 역직렬(@JsonTypeInfo, @JsonSubTypes)

탄생 2023. 6. 25. 19:31

❖ 사용안

계층구조에서 다른 하위클래스를 여러개 가지고 있을 경우 이를 처리하기 위해서 사용합니다.

예를 들어 동물 하위에 강아지, 고양이, 토끼라는 하위 클래스가 있고 이들이 가지고 있는 필드와 처리하는 함수가 다를 경우 이를 하나의 필드로 구분하여 사용할 수 있습니다.

❖ 어노테이션 설명

@JsonTypeInfo : 다형성을 지원하는 Jackson 라이브러리로 json 직렬, 역직렬을 지원합니다.

  • use : 다형성을 나타내는 정보를 어떻게 사용할지 지정합니다. 
    - JsonTypeInfo.Id.NAME : 타입 이름을 사용하여 다형성을 처리합니다.
    - JsonTypeInfo.Id.CLASS: 클래스 정보를 사용하여 다형성을 처리합니다.
    - JsonTypeInfo.Id.MINIMAL_CLASS: 최소한의 클래스 정보만 사용하여 다형성을 처리합니다. 클래스 이름을 속성으로 포함시킵니다.
    - JsonTypeInfo.Id.NONE: 다형성 정보를 사용하지 않습니다.
  • include : 다형성 정보를 어떤 방식으로 포함할지 지정
    - JsonTypeInfo.As.PROPERTY: 다형성 정보를 JSON 속성으로 포함시킵니다.
    - JsonTypeInfo.As.EXISTING_PROPERTY: 기존의 JSON 속성을 사용하여 다형성 정보를 포함시킵니다.
    - JsonTypeInfo.As.WRAPPER_OBJECT: 다형성 정보를 JSON 객체로 감싸서 포함시킵니다.
    - JsonTypeInfo.As.WRAPPER_ARRAY: 다형성 정보를 JSON 배열로 감싸서 포함시킵니다.
    - JsonTypeInfo.As.EXTERNAL_PROPERTY: 외부 속성에 다형성 정보를 포함시킵니다.
  • property : 다형성 정보를 포함할 JSON 속성의 이름을 지정합니다.
  • visible : 다형성 정보가 JSON에 노출되어야 하는지 여부를 지정합니다.(기본값은 true)

@JsonSubTypes : 다형성의 서브타입을 지정

  • value : 매핑할 하위 클래스를 지정합니다.
  • name : 해당 하위 클래스에 대한 이름을 지정합니다.

❖ 예제

objectMapper를 통해 snake_case로 정의할 수 있습니다.
이때 JsonTypeInfo.property 도 skake_case로 정의되어야 합니다.
보통 프론트와 통신시 json필드를 snake_case로 규칙을 정의하므로 해당 옵션을 추가하였습니다.

import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.suby.kotlin.json.subtype.AnimalType.CAT
import net.suby.kotlin.json.subtype.AnimalType.DOG
import net.suby.kotlin.json.subtype.AnimalType.RABBIT

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "animal_type")
@JsonSubTypes(
    JsonSubTypes.Type(value = Dog::class, name = "DOG"),
    JsonSubTypes.Type(value = Cat::class, name = "CAT"),
    JsonSubTypes.Type(value = Rabbit::class, name = "RABBIT"),
)
sealed class Animal(
    val animalType: AnimalType,
    open val name: String,
)

data class Dog(
    override val name: String,
    val color: String,
) : Animal(DOG, name)

data class Cat(
    override val name: String,
    val howling: String,
) : Animal(CAT, name)


data class Rabbit(
    override val name: String,
    val numberOfLegs: Int,
) : Animal(RABBIT, name)

data class AnimalHospital(
    val name: String,
    val animal: Animal,
) {
    val dog: Dog
        get() = animal as Dog
    val cat: Cat
        get() = animal as Cat
    val rabbit: Rabbit
        get() = animal as Rabbit
}

fun main() {
    val objectMapper = ObjectMapper()
        .registerModule(KotlinModule.Builder().build())
        .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)  // snake_case로 변환

    val dog = Dog("멍뭉이", "흰색")
    val cat = Cat("냥옹이", "야옹")
    val rabbit = Rabbit("토순이", 4)

    // Serialize dog to JSON
    val dogJson = objectMapper.writeValueAsString(dog)
    println(dogJson)
    val catJson = objectMapper.writeValueAsString(cat)
    println(catJson)
    val rabbitJson = objectMapper.writeValueAsString(rabbit)
    println(rabbitJson)

    // Deserialize JSON to animal object
    val deserializedDog = objectMapper.readValue(dogJson, Animal::class.java)
    val deserializedCastDog = deserializedDog as Dog
    val deserializedCat = objectMapper.readValue(catJson, Animal::class.java)
    val deserializedCastCat = deserializedCat as Cat
    val deserializedRabbit = objectMapper.readValue(rabbitJson, Animal::class.java)
    val deserializedCastRabbit = deserializedRabbit as Rabbit
    println(deserializedDog)
    println(deserializedCastDog)
    println(deserializedCat)
    println(deserializedCastCat)
    println(deserializedRabbit)
    println(deserializedCastRabbit)

    val dogHospital = AnimalHospital("강아지 병원", dog)
    val catHospital = AnimalHospital("고양이 병원", cat)
    val rabbitHospital = AnimalHospital("토끼 병원", rabbit)
    val animalHospitals = listOf(dogHospital, catHospital, rabbitHospital)
    val firstAnimalHospital = animalHospitals[0].dog
    println(firstAnimalHospital.animalType)
    println(firstAnimalHospital.color)
}

실행 결과

{"name":"멍뭉이","color":"흰색","animal_type":"DOG"}
{"name":"냥옹이","howling":"야옹","animal_type":"CAT"}
{"name":"토순이","number_of_legs":4,"animal_type":"RABBIT"}
Dog(name=멍뭉이, color=흰색)
Dog(name=멍뭉이, color=흰색)
Cat(name=냥옹이, howling=야옹)
Cat(name=냥옹이, howling=야옹)
Rabbit(name=토순이, numberOfLegs=4)
Rabbit(name=토순이, numberOfLegs=4)
DOG
흰색