문제 : Kotlinx Serialization 에서 MissingFieldException가 발생 하였다.
DTO에 키가 있는데 Response에 key가 없어서 발생한 사례입니다.
안드로이드 개발에서 많이 쓰이는 Gson과 Kotlinx Serialization, 두 라이브러리는 이 과정에서 눈에 띄는 차이를 보입니다.
특히 JSON에 특정 키가 없거나 null 값이 올 때 어떻게 동작하는지 이해하는 것이 중요합니다.
잘못하면 앱이 죽는(크래시) 원인이 될 수 있거든요.
그리고, 추가키(정의되지 않는 키)가 내려와도 UnknownFieldException이 발생한다.
왜 예외가 나나? (kotlinx.serialization의 규칙)
1. 필드가 빠지면: MissingFieldException
- DTO에 정의된 프로퍼티(필드)가 JSON 응답에 아예 없으면 MissingFieldException이 발생합니다.
- [❌ 가장 많이 혼동하는 부분] val name: String? 처럼 타입을 널러블(?)로 선언하는 것만으로는 충분하지 않습니다.
- val name: String? : "이 필드는 null 값을 허용한다"는 뜻이지, "이 필드가 없어도 된다(Optional)"는 뜻이 아닙니다. 이 필드는 **여전히 필수(Required)**입니다. JSON에 name 키가 아예 없으면 MissingFieldException이 발생합니다.
- val name: String? = null : "이 필드는 null 값을 허용하며, JSON에 키가 없어도 된다"는 뜻입니다. 키가 없으면 기본값인 null이 할당됩니다.
- 결론: 필드를 '옵션(Optional)'으로 만들고 싶다면, 반드시 = null 이나 = -1 같은 명시적인 기본값을 할당해야 합니다.
2. 추가(모르는) 키가 있으면: UnknownFieldException
- Gson은 DTO에 정의되지 않은 키(e.g., "new_ad_field": "...")가 JSON에 있어도 조용히 무시하고 파싱을 성공시킵니다.
- 하지만 kotlinx.serialization은 기본적으로 DTO에 정의되지 않은 키가 오면 "이건 내가 모르는 필드"라며 UnknownFieldException 예외를 발생시킵니다.
- 이는 서버 API 명세가 바뀌었음을 개발자에게 알려주는 안전장치 역할을 하기도 합니다.
해결 방법 : ignoreUnknownKeys 추가. 아래 방법 있습니다.
Gson은 왜 안 터지지?
Gson은 기본적으로 관대(lenient) 합니다.
없는 항목은 각 타입의 기본값으로 채우고(객체=null, 숫자=0, 불린=false), 파싱을 이어갑니다.
그래서 “파싱 시점”엔 잘 지나가고, 나중에 그 값을 쓸 때 NPE가 날 수 있죠.
Gson = 관대: 누락/null/추가 키가 와도 파싱은 대체로 성공. 대신 값이 기본값/null로 들어가고, 나중 사용 시 NPE가 터질 수 있음.
kotlinx.serialization = 엄격: 기본값 없는 프로퍼티는 필수로 취급 → 누락/null이면 즉시 예외. 대신 DTO 기본값이나 옵션을 명시하면 안전.
| JSON 상황 | Gson 결과 | kotlinx.serialization 결과(기본설정) | 이유 | 실무 해결 |
| 키가 없음 ("order" 자체 없음) | 파싱 성공. 프리미티브는 기본값(0/false), 레퍼런스는 null | 예외: MissingFieldException (기본값이 없으면 필수 필드) | Gson은 관대/리플렉션, Kotlinx는 생성자기반+옵셔널 판정 | DTO에 기본값 넣어 옵션화 val order: Int = -1 |
| 값이 null ("order": null) | 프리미티브(비-null Int/Boolean)는 기본값(0/false), 레퍼런스/nullable은 null (파싱 성공) | 예외: JsonDecodingException (non-null에 null 불가) | non-null은 null 불허 | 1. explicitNulls=false로 null을 ‘부재’처럼 처리 → 기본값 적용, 2. 또는 Int?로 받고 매퍼에서 ?: -1 |
| 추가(모르는) 키 | 보통 무시 | 예외 (기본값) | 엄격 모드라 미정의 키 오류 | Json { ignoreUnknownKeys = true } |
| 타입 불일치 ("order":"5") | 케이스에 따라 시도/실패 | 예외: JsonDecodingException | 타입 안 맞음 | 서버 수정 or 커스텀 시리얼라이저로 보정 |
- Kotlinx에서 non-null이면 예외를 “던진다”(자동 예외처리 X). DTO 기본값이 없고 키가 없으면 예외를 “던진다”.
설정/코드로 안전하게 쓰는 법 (kotlinx.serialization)
1) “키 없음”에 안전해지는 가장 쉬운 방법
@Serializable
data class HomeSectionDto(
@SerialName("order") val order: Int = -1, // 기본값 → 옵션 필드로 간주
@SerialName("section") val section: String = "",
// 하위 DTO도 기본값 제공 필요
)
- 기본값이 있으면 → 그 프로퍼티는 옵셔널이라 키가 없어도 통과.
2) "order": null도 예외 없이 받으려면
옵션 A — 전역 설정 완화
val json = Json {
explicitNulls = false // null을 '없는 것'처럼 취급 → 기본값 사용
}
- explicitNulls = false → 명시적 null을 부재처럼 취급해 기본값/nullable 로 대입.
옵션 B — 프로퍼티를 nullable로 받고 매퍼에서 보정
@Serializable
data class HomeSectionDto(@SerialName("order") val order: Int? = null)
fun HomeSectionDto.asDomain() = HomeSection(order = order ?: -1)
- 전역 설정을 건드리지 않고 명시적으로 제어할 수 있음.
3) 추가 키가 와도 무시하고 싶다면 UnknownFieldException
val json = Json { ignoreUnknownKeys = true }
Retrofit 설정 전역 Kotlinx 설정 예
@OptIn(ExperimentalSerializationApi::class)
@Provides
fun provideResponseJsonConverterFactory(): Converter.Factory =
Json {
ignoreUnknownKeys = true // 추가 키 무시
explicitNulls = false // "null"을 부재 취급 → 기본값/nullable로
}.asConverterFactory("application/json".toMediaType())
궁금했던 것
Q. Gson이면 null-safe 안 해도 되나요?
A. 아니요. 파싱은 통과하지만 레퍼런스 타입에 null이 들어오면 사용 시 NPE로 죽습니다.
Q. Kotlinx에서 non-null 프로퍼티면 null은?
A. 예외를 던집니다. 자동 예외처리(흡수) 아닙니다.
Q. "order": null을 기본값으로 바꾸고 싶어요.
A. explicitNulls=false + val order: Int = -1 or val order: Int? = null + 매퍼 보정.
'안드로이드 > 에러' 카테고리의 다른 글
| [Android] ArrayList에서 ConcurrentModificartionException 발생 (0) | 2024.09.20 |
|---|---|
| [Android] Gson으로 데이터를 가져오지 못할 때 (코드 난독화) (0) | 2024.03.20 |
| binding시 release 모드에서 에러가 발생할 때 (0) | 2023.09.20 |
| 안드로이드 스튜디오 AVD is already running 오류 해결하기 (0) | 2023.08.01 |
| [Android] 프래그먼트 빈생성자가 없어서 생긴 문제 ContextAwareHelper (0) | 2023.04.21 |