Introduction
Retrofit with Moshi converter parses JSON API responses into Kotlin data classes. When the JSON structure does not exactly match the data class definition, Moshi throws JsonDataException for missing required fields, silently sets nullable fields to null, or fails on unknown enum variants and polymorphic types. Common issues include mismatched property names (snake_case JSON vs camelCase Kotlin), non-nullable fields for JSON values that can be null, and API error responses that return a different JSON structure than success responses.
Symptoms
JsonDataException: Required field is absentfor optional API fields- Fields set to null despite JSON containing values
JsonDataException: Cannot skip unexpected valuefor unknown types- Retrofit
onResponsefires but body is null - Error response body cannot be parsed into error data class
- Polymorphic JSON fails to deserialize
Error output: ``` com.squareup.moshi.JsonDataException: Required value 'email_address' missing at $.user at com.squareup.moshi.internal.Util.throwMissingProperty
Retrofit response error body: {"error": "invalid_token", "message": "Token expired"} Failed to parse: com.example.ErrorResponse ```
Common Causes
- JSON field name does not match Kotlin property name
- Non-nullable Kotlin property for a JSON field that can be null
- Moshi strict mode rejects unknown fields
- API returns different JSON structure for error vs success responses
- Enum variant in JSON not defined in Kotlin enum class
- Date format mismatch between API and Moshi adapter
Step-by-Step Fix
- 1.Use @Json annotation for field name mapping:
- 2.```kotlin
- 3.// API returns snake_case, Kotlin uses camelCase
- 4.data class UserResponse(
- 5.@Json(name = "user_id")
- 6.val userId: Int,
@Json(name = "email_address") val emailAddress: String,
@Json(name = "full_name") val fullName: String,
@Json(name = "created_at") val createdAt: String // Parse as string, convert to Instant manually )
// Use Moshi with Kotlin codegen for automatic snake_case support val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()
// Or define a custom naming policy val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .add( FieldNamingPolicy.SNAKE_CASE // Auto-converts camelCase to snake_case ) .build() ```
- 1.Handle nullable and optional fields correctly:
- 2.```kotlin
- 3.// WRONG - non-nullable for optional field
- 4.data class UserResponse(
- 5.val id: Int,
- 6.val name: String,
- 7.val bio: String // API sometimes returns null -> JsonDataException!
- 8.)
// CORRECT - nullable for fields that can be null data class UserResponse( val id: Int, val name: String, val bio: String? = null // Nullable with default )
// For truly optional fields (may be absent from JSON entirely) data class UserResponse( val id: Int, val name: String, val bio: String? = null, val avatar: String? = null, val preferences: UserPreferences? = null )
// Use @Json(ignore = true) for computed fields data class UserResponse( val id: Int, val firstName: String, val lastName: String, @Json(ignore = true) val fullName: String = "$firstName $lastName" // Not in JSON ) ```
- 1.Handle unknown fields and enum variants:
- 2.```kotlin
- 3.// Moshi rejects unknown fields by default — use lenient mode
- 4.val moshi = Moshi.Builder()
- 5..add(KotlinJsonAdapterFactory())
- 6..build()
val retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(MoshiConverterFactory.create(moshi)) .build()
// For enums with unknown variants enum class UserRole { ADMIN, USER, MODERATOR, @Json(name = "super_admin") SUPER_ADMIN, UNKNOWN // Catch-all for new server-side roles }
// Custom adapter for unknown enum values class UnknownEnumAdapter<T : Enum<T>>( private val enumClass: Class<T>, private val unknownValue: T ) : JsonAdapter<T>() { @FromJson override fun fromJson(reader: JsonReader): T { val name = reader.nextString() return enumConstantByName(name) ?: unknownValue }
@ToJson override fun toJson(writer: JsonWriter, value: T) { writer.value(value.name.lowercase()) }
private fun enumConstantByName(name: String): T? = enumClass.enumConstants?.find { it.name.equals(name, ignoreCase = true) } }
// Register the adapter val moshi = Moshi.Builder() .add(UnknownEnumAdapter(UserRole::class.java, UserRole.UNKNOWN)) .add(KotlinJsonAdapterFactory()) .build() ```
- 1.**Parse error response body separately":
- 2.```kotlin
- 3.interface ApiService {
- 4.@GET("users/{id}")
- 5.suspend fun getUser(@Path("id") id: Int): Response<UserResponse>
- 6.}
// Usage try { val response = apiService.getUser(userId)
if (response.isSuccessful) { val user = response.body() // Parsed as UserResponse showUser(user) } else { // Parse error body val errorBody = response.errorBody()?.string() val errorResponse = moshi.adapter(ApiError::class.java).fromJson(errorBody) showError(errorResponse?.message ?: "Unknown error") } } catch (e: Exception) { // Network error, timeout, etc. showError("Network error: ${e.message}") }
data class ApiError( val error: String, val message: String, val code: Int? = null )
// Generic error handling wrapper sealed class ApiResult<out T> { data class Success<T>(val data: T) : ApiResult<T>() data class Error(val code: Int, val message: String) : ApiResult<Nothing>() data class NetworkError(val exception: Throwable) : ApiResult<Nothing>() }
suspend fun <T> safeApiCall(call: suspend () -> Response<T>): ApiResult<T> { return try { val response = call() if (response.isSuccessful) { ApiResult.Success(response.body()!!) } else { val errorBody = response.errorBody()?.string() val apiError = moshi.adapter(ApiError::class.java).fromJson(errorBody) ApiResult.Error(response.code(), apiError?.message ?: "Unknown error") } } catch (e: Exception) { ApiResult.NetworkError(e) } } ```
Prevention
- Always use
@Json(name = "...")for fields with different JSON names - Make all fields nullable that can be null in the API response
- Use sealed classes for API results (success vs error vs network)
- Parse error bodies separately from success bodies
- Add Moshi's
KotlinJsonAdapterFactoryfor Kotlin data class support - Test API responses with both success and error JSON structures
- Log raw JSON in debug builds for easier debugging of parse failures