Kotlin

Kotlinx Serialization의 JsonBuilder Properties 알아보기

MJ핫산 2023. 2. 23. 18:46

프로젝트에서 서버 통신 후 받은 Response를 문자열(String)으로 웹뷰에 전달해야하는 일이 있어서 kotlinx-serialization의 StringFormat.encodeToString(value: T): String을 사용했다. 그런데 서버에서 전달된 값이 객체에 잘 저장이 되고 있었음에도 불구하고 encodeToString를 하면 키-값이 모두 사라져있는 것이었다. 😱

// 이런 상황이었다. (예시)

@Serializable
data class Foo(
    val a: Int = 0,
    val b: Int = 0,
)

val response = Foo(a = 42, b = 0)
Json.encodeToString(response) // {"a": 42} // 오잉 b는 어디갔지 ㄴㅇㄱ ??

결론적으로는 JsonBuilder의 encodeDefaults 옵션을 true로 변경해서 해결했다! 🥳 (자세한 설명은 아래 참고)

이번 이슈 덕분에 다양한 JsonBuilder Properties를 알게 되어서 자축의 의미로 블로그에 정리해보려고 한다.

https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/

 

JsonBuilder

Removes JSON specification restriction (RFC-4627) and makes parser more liberal to the malformed input. In lenient mode quoted boolean literals, and unquoted string literals are allowed.

kotlinlang.org

 

JsonBuilder

class JsonBuilder
// Builder of the Json instance provided by Json { ... } factory function

우선 Json Format을 설정하기 위해서는 Json 객체를 이용해야하는데, JsonBuilder를 통해서 객체 생성과 추가적인 값(property) 설정을 할 수 있다.

 

Properties

allowSpecialFloatingPointValues 🔗

var allowSpecialFloatingPointValues: Boolean

NaN이나 infinites 같은 특별한 floating-point(부동소수점)들에 대한 JSON 규격 제한을 제거하고, 직렬화 및 역직렬화를 할 수 있도록 한다. 그래서 이 옵션을 활성화 할 때는 수신하는 쪽이 이런 특수한 값들을 직렬화/역직렬화 할 수 있는지 확인 후 사용해야한다. 기본값은 false이다.

 

allowStructuredMapKeys 🔗

var allowStructuredMapKeys: Boolean

JSON에서 사용되는 key는 기본적으로 String 타입이라 직렬화할 때 key로 사용되는 부분은 primitive 값이거나 enum이어야 한다. 그래서 기본적으로는 map으로 된 형태는 직렬화할 수 없는데, 이 옵션을 활성화하면 [k1, v1, k2, v2] 이런 식으로 map의 key와 value를 순서대로 나열하는 JsonArray 형태로 표현할 수 있다. 기본 값은 false이다.

 

@Serializable
data class Test(val key: String)

val map = mapOf(
    Test("key1") to "value1",
    Test("key2") to "value2"
)

Json { allowStructuredMapKeys = true }.encodeToString(map) // "[{"key":"key1"},"value1",{"key":"key2"},"value2"]"

 

classDiscriminator 🔗

var classDiscriminator: String

다형성 직렬화를 위한 클래스 설명자 속성의 이름이다. 기본 값은 “type”이다.

프로퍼티에 대한 설명만 봐서는 어떤 역할을 하는지 전혀 감이 오지 않는다..😩 이래저래 찾아봤는데 글로 설명하는 것보다 예제를 보는게 더 이해가 잘 되는 것 같다. 아래 예제는 각각 classDiscriminator를 이용해서 encode/decode를 하는 코드이다. 부디 이해가 잘 됐으면….

@Serializable
sealed class Book

@Serializable
@SerialName("comics")
data class Comics(
    val page: Int,
    val character: String,
): Book()

@Serializable
@SerialName("IT")
data class Computer(
   val language: String,
): Book()

// decode from string
val stringData = """
    [{"kind":"comics","page":300,"character":"Iron Man"},{"kind":"IT","language":"Kotlin"}]
"""

Json { classDiscriminator = "kind" }.decodeFromString<List<Book>>(stringData) // [Comics(page=300, character=Iron Man), Computer(language=Kotlin)]

// encode to string
val objectData: Book = Computer("Java")

Json { classDiscriminator = "kind" }.encodeToString(objectData) // {"kind":"IT","language":"Java"}

 

coerceInputValues 🔗

var coerceInputValues: Boolean

기본값은 false인데, 이 옵션을 활성화하면 다음과 같이 잘못된 JSON 상황에서 속성의 기본 값을 강제 적용한다.

  • property의 타입이 non-null 인데 JSON 값이 nullable일 때
  • property의 타입이 enum인데 JSON 값에 알 수 없는 enum member가 있는 경우

 

encodeDefaults 🔗

var encodeDefaults: Boolean

내 이슈의 원인이자 해결방법이 되어준 옵션!!

기본값을 인코딩할지 안할지 여부를 정할 수 있고, 기본값은 false이다.

@Serializable
data class Foo(
    val a: Int = 0,
    val b: Int = 0,
)

val response = Foo(a = 42, b = 0)

Json { encodeDefaults = false }.encodeToString(response) // {"a": 42} // encodeDefaults 값이 false인데 b의 값이 기본값과 같아서 인코딩되지 않았다
Json { encodeDefaults = true }.encodeToString(response)  // {"a": 42, "b":0}

 

explicitNulls 🔗

@ExperimentalSerializationApi
var explicitNulls: Boolean

생성된 JSON 문자열의 크기를 줄이는 또 다른 방법은 null 값을 생략하는 것이다. 이 옵션은 property의 값이 null이었을 때 직렬화된 JSON 문자열에 null을 포함할지 여부를 결정한다. 기본 값을 true이다.

val format = Json { explicitNulls = false }
val flutterExpert = Expert(id = 1, "Nav", Category.FLUTTER, null)
val jsonFlutterExpert = format.encodeToString(flutterExpert)
println("EncodedResult: $jsonFlutterExpert")
println("DecodedResult: ${format.decodeFromString<Expert>(jsonFlutterExpert)}")

/*
EncodedResult: 
{
  "id":1,
  "name":"Nav",
  "category":"FLUTTER"
}
DecodedResult: 
Expert(
  id=1, 
  name=Nav,
  category=FLUTTER, 
  publishedArticles=null, 
  recentEventLink=null
)
*/

 

ignoreUnknownKeys 🔗

var ignoreUnknownKeys: Boolean

직렬화/역직렬화를 할 때 서버의 변경 등의 이유로 정확한 property를 맞추지 못할 때가 있다. 예를들어 버전이 바뀌면서 값이 추가될 수도, 삭제될 수도 있다는 것이다. 이때 기본 JSON 객체를 사용한다면 json key와 kotlin model의 property들이 완벽하게 짝이 맞아야 에러없이 동작한다. 그래서 만약 짝이 맞지 않아도 그냥 무시하고 넘어가고 싶다면 ignoreUnknownKeys를 활성화 시키면 된다. 기본값은 false이다. (+ 전달된 데이터에 원하는 property가 없을 때 기본값이라도 넣고 싶다면 kotlin model에 기본값을 넣어두면 된다!)

 

isLenient 🔗

var isLenient: Boolean

이 옵션을 허용할 경우 JSON 규격 제한(RFC-4627)이 제거된다. 기본적으로 JSON에서 사용되는 key 값은 따옴표(””)로 감싸져야 하고 value 또한 enum, string 타입일 경우 따옴표로 감싸져야 한다. 하지만 isLenient를 true로 바꿔주면 따옴표에 대한 규칙을 느슨하게 체크한다. 기본값은 false이다.

@Serializable
data class Person(val name: String)

val data = """
    {
        name: minji
    }
"""

Json { isLenient = true }.decodeFromString<Person>(data) // Person(name=minji)

 

prettyPrint 🔗

var prettyPrint: Boolean

JSON 직렬화 결과를 예쁘게 출력할지에 대한 여부를 지정하는 옵션으로 기본값은 false이다.

@Serializable
data class Comics(
    val page: Int,
    val character: String,
)

val data: Book = Comics(300, "Iron Man")

Json { prettyPrint = true }.encodeToString(data)
// 결과(예쁘게 출력된다) ⬇️
// {
//     "type": "comics",
//     "page": 300,
//     "character": "Iron Man"
// }

 

prettyPrintIndent 🔗

@ExperimentalSerializationApi
var prettyPrintIndent: String

기본 Indent(공백 4칸) 대신 사용할 들여쓰기 문자열을 지정할 수 있다. prettyPrint가 활성화되어있을 때 사용할 수 있으며 아직 실험적 API 이다. (23.02.23 기준)

⛔️ 주의사항!!

prettyPrintIndent에는 공백, 탭, 줄바꿈, CF(carriage return)만 사용할 수 있다.

kotlinx.serialization.json.Json.kt

@Serializable
data class Comics(
    val page: Int,
    val character: String,
)

val data: Book = Comics(300, "Iron Man")

Json { 
    prettyPrint = true
		prettyPrintIndent = "  " // 기본 공백 4칸인데 2칸으로 수정
}.encodeToString(data)
// 결과(예쁘게 출력된다 + Indent: 공백 2칸) ⬇️
// {
//   "type": "comics",
//   "page": 300,
//   "character": "Iron Man"
// }

 

useAlternativeNames 🔗

var useAlternativeNames: Boolean

Json 인스턴스에서 JsonNames 주석을 사용할지 말지 여부를 지정하는 옵션으로 기본값은 true이다. JsonNames 어노테이션을 전혀 사용하고 있지 않을 때 이 플래그를 false로 설정하면 성능이 향상될 수 있지만 정의되지 않은 많은 key들이 건너뛰어질 수 있다.

 

마무리

JsonBuilder에 이렇게 많은 프로퍼티들이 있었구나.. 하게 되는 시간이었다. 프로젝트를 할 때 보통 ignoreUnknownKeys, encodeDefaults, coerceInputValues, isLenient 정도만 사용했었는데 다른 옵션들도 잘 활용한다면 JSON 직렬화/역직렬화를 할 때 정말 유용하게 활용할 수 있을 것 같았다. 끝!

 

참고

- https://tourspace.tistory.com/367

 

[Kotlinx serialization] Json 직렬화/역직렬화 -JSON features #6

기본적으로 Json 객체의 구현은 input값에 엄격하고, kotlin type safety를 준수하며, JSON을 표준으로 표현하기 위해서 serialize 할 수 있는 kotlin 값들을 제한합니다. 다시말해, Kotlin의 value와 모델이 정확

tourspace.tistory.com

- https://blog.mathpresso.com/%EC%8B%A0%EC%9E%85-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%9D%98-kotlinx-serialization-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EC%84%9C%EC%82%AC%EC%8B%9C-740597911e2e

 

신입 안드로이드 개발자의 kotlinx.serialization 리팩토링 서사시

제로에서 시작하는 개발세계 리팩토링 생활

blog.mathpresso.com

- https://proandroiddev.com/kotlin-kotlinx-serialization-1-3-e41313dcb4d2

 

What’s New in kotlinx.serialization 1.3

In this article, we will learn about the major new features that kotlinx.serialization 1.3 brings for developers to manage JSON parsing…

proandroiddev.com

 

'Kotlin' 카테고리의 다른 글

Kotlin Delegation 알아보기(1) - Delegated Class  (0) 2023.02.16