Introduction
Moshi strictly enforces that non-nullable Kotlin properties must have a corresponding non-null value in the JSON. When an API omits a required field or returns null for a non-nullable property, Moshi throws JsonDataException: Required property 'x' absent. This commonly happens when APIs evolve, when different endpoints return slightly different response shapes, or when optional fields are documented but not clearly marked.
Symptoms
com.squareup.moshi.JsonDataException: Required property 'email' absentNon-null value 'null' for property 'name'when API returns null- Works for some API responses but fails for others
- Deserialization succeeds in development but fails in production
- Error does not indicate which endpoint or response caused the issue
Error output:
``
com.squareup.moshi.JsonDataException: Required property 'phone' absent
at $.phone (MyData.kt:8)
at com.squareup.moshi.internal.Util.missingProperty(Util.java:616)
Common Causes
- API does not send a field that the Kotlin model requires
- API returns
nullfor a field declared as non-nullable - New API version removes previously required fields
- Different endpoints return different response shapes with same model
- JSON field name mismatch (snake_case vs camelCase)
Step-by-Step Fix
- 1.**Make properties nullable for optional API fields":
- 2.```kotlin
- 3.import com.squareup.moshi.Json
- 4.import com.squareup.moshi.JsonClass
// WRONG - all fields required, fails if any are missing @JsonClass(generateAdapter = true) data class User( val id: Long, val name: String, val email: String, val phone: String, // May be missing from API val avatar: String // May be null from API )
// CORRECT - make optional fields nullable @JsonClass(generateAdapter = true) data class User( val id: Long, val name: String, val email: String, val phone: String? = null, // Optional, defaults to null val avatar: String? = null // Nullable, handles null in JSON ) ```
- 1.**Use Moshi adapters for default values":
- 2.```kotlin
- 3.import com.squareup.moshi.FromJson
- 4.import com.squareup.moshi.ToJson
// Custom adapter for handling missing fields with defaults class DefaultStringAdapter { @FromJson fun fromJson(string: String?): String { return string ?: "" // Default empty string instead of null }
@ToJson fun toJson(value: String): String? { return value.takeIf { it.isNotEmpty() } } }
val moshi = Moshi.Builder() .add(DefaultStringAdapter()) .add(KotlinJsonAdapterFactory()) .build() ```
- 1.**Handle field name mismatches":
- 2.```kotlin
- 3.@JsonClass(generateAdapter = true)
- 4.data class User(
- 5.@Json(name = "user_id") val id: Long,
- 6.@Json(name = "full_name") val name: String,
- 7.@Json(name = "email_address") val email: String,
- 8.val phone: String? = null // No @Json needed if names match
- 9.)
// API response: // {"user_id": 1, "full_name": "John", "email_address": "john@example.com"} ```
- 1.**Use a lenient JSON adapter for tolerant parsing":
- 2.```kotlin
- 3.import com.squareup.moshi.JsonAdapter
- 4.import com.squareup.moshi.JsonReader
- 5.import com.squareup.moshi.JsonWriter
- 6.import com.squareup.moshi.Moshi
- 7.import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
// Tolerant adapter that skips unknown fields and handles missing required ones object TolerantAdapterFactory { fun create(moshi: Moshi): Moshi { return moshi.newBuilder() .add(TolerantKotlinFactory()) .build() } }
// Use Moshi's built-in leniency val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()
val adapter = moshi.adapter(User::class.java) .lenient() // Allows top-level values, comments, etc. ```
Prevention
- Declare optional fields as nullable with default values
- Use
@Json(name = "...")for fields with different JSON names - Write deserialization tests against real API responses
- Monitor JsonDataException in production for API changes
- Use Moshi codegen (
@JsonClass(generateAdapter = true)) for better performance - Document which API fields are required vs optional in model classes