Intermediate: Android basics · Saving data and calling APIs · topic 33 of 50 · 30 min
JSON models and handling errors
Explanation
JSON to Kotlin
JSON (JavaScript Object Notation) is the text format most APIs use: objects in braces, lists in square brackets, keys in quotes. kotlinx.serialization is JetBrains' official library for converting JSON to Kotlin classes and back, using a compiler plugin so no slow reflection is needed.
{
"order_id": 9081,
"status": "SHIPPED",
"items": [ { "name": "Kurta", "qty": 1 } ],
"coupon": null,
"eta_days": 3
}@Serializable
data class OrderDto(
@SerialName("order_id") val orderId: Int,
val status: String,
val items: List<ItemDto>,
val coupon: String? = null, // may be null or missing
@SerialName("eta_days") val etaDays: Int = 0,
)
@Serializable
data class ItemDto(val name: String, val qty: Int)
val json = Json { ignoreUnknownKeys = true }
val order = json.decodeFromString<OrderDto>(responseText)DTO means Data Transfer Object: a class that matches the API's shape exactly. Many teams convert DTOs into their own app models in the repository, so if the server renames a field, only the DTO and the mapping change, not every screen.
Mapping is a small function, often an extension like fun OrderDto.toOrder() = Order(id = orderId, ...), that converts one class into the other. It is also the right place to clean data: trim names, turn status strings into an enum, or convert paise to a display amount.
Why ignoreUnknownKeys?
Servers evolve. If the backend adds a field like "gift_wrap" and your app's class does not have it, the default Json throws an error on unknown keys. Phones running an older app version would suddenly fail. ignoreUnknownKeys = true tells the parser to skip fields it does not know.
Things that go wrong
| Failure | Typical exception | What to show |
|---|---|---|
| No internet, DNS failure, timeout | IOException (for example UnknownHostException, SocketTimeoutException) | "Check your internet connection" + Retry |
| Server says no (404, 500...) | retrofit2.HttpException with code() | "Not found" or "Server problem, try later" |
| JSON does not match the model | SerializationException | Log it; show a generic error |
Users do not care which exception happened; they care what to do next. Translate each technical failure into a short message and an action: retry, check the connection, or try again later. Log the full technical details for yourself with Log or a crash-reporting tool.
UI states with a sealed interface
sealed interface ProductsUiState {
data object Loading : ProductsUiState
data class Success(val products: List<Product>) : ProductsUiState
data class Error(val message: String) : ProductsUiState
}
class ProductsViewModel(private val repo: ProductRepository) : ViewModel() {
private val _state = MutableStateFlow<ProductsUiState>(ProductsUiState.Loading)
val state: StateFlow<ProductsUiState> = _state.asStateFlow()
init { load() }
fun load() {
viewModelScope.launch {
_state.value = ProductsUiState.Loading
_state.value = try {
ProductsUiState.Success(repo.productsIn("sarees"))
} catch (e: IOException) {
ProductsUiState.Error("No internet connection")
} catch (e: HttpException) {
ProductsUiState.Error("Server error (${e.code()})")
}
}
}
}@Composable
fun ProductsScreen(state: ProductsUiState, onRetry: () -> Unit) {
when (state) {
ProductsUiState.Loading -> CircularProgressIndicator()
is ProductsUiState.Success -> LazyColumn { items(state.products, key = { it.id }) { Text(it.name) } }
is ProductsUiState.Error -> Column {
Text(state.message)
Button(onClick = onRetry) { Text("Retry") }
}
}
}Common mistakes
- No error handling: the app crashes the first time the user is in a lift or metro tunnel.
- Catching Exception broadly, which also swallows coroutine cancellation. Catch IOException and HttpException.
- Showing raw technical messages ("java.net.UnknownHostException") to users.
- Using the default Json without ignoreUnknownKeys for a server you do not control.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
JSON models and handling errors · 1/6
kotlinx serialization turns JSON into serializable data classes.
Prefer a video by a teacher? Find free videos on “kotlinx serialization retrofit error handling ui state loading error android” on YouTube ↗
In real life: Bus timings in a patchy network
A city bus app is used by commuters with weak signals at bus stops. Its arrivals screen shows a progress spinner while loading, the list on success, and on failure a friendly "No internet. Showing last saved timings" with a Retry button. The sealed UiState makes sure no screen state is forgotten, and ignoreUnknownKeys keeps older app versions working when the transport department adds new fields.
@Serializable
data class ArrivalDto(@SerialName("route_no") val routeNo: String, @SerialName("eta_min") val etaMin: Int)
fun ArrivalDto.toLabel() = "Route $routeNo in $etaMin min"Practice (0/3 done)
The server adds a new JSON field your class does not have. Your Json has ignoreUnknownKeys = true. What happens?
Which exception usually means "no internet" when calling Retrofit?
Write a WeatherUiState sealed interface (Loading, Success(tempC, city), Error(message)), a ViewModel that loads weather through a repository and maps IOException and HttpException to messages, and a screen with a Retry button. Test it by turning on airplane mode.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Offline-first apps and DataStoreSaving data and calling APIs · same module
- Room databaseSaving data and calling APIs · same module
- JSON modelsFlutter and Dart · Networking, storage and Firebase
- Dart 3: enums, records, patterns and sealed classesFlutter and Dart · Null safety and object-oriented Dart
- Handling errors wellBackend Development · APIs and request handling
- Loading, errors and slow networksFull-Stack Development · Connecting frontend and backend
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does @SerialName("eta_days") do?
Answer: Maps the JSON key eta_days to the Kotlin property
It connects different naming styles.
How do you make a JSON field optional in the model?
Answer: Give it a nullable type and/or a default value
Defaults and nullability cover missing fields.
Why is catching Exception broadly risky in coroutines?
Answer: It can swallow CancellationException and break cancellation
Catch specific exceptions.
What is a DTO?
Answer: A class that matches the API's data shape
Data Transfer Object.
Why convert JSON into model classes?Related
Answer: So the compiler checks field names and types everywhere else
Type safety after one parsing step.
From JSON models
Why can a when over a sealed type skip else?Related
Answer: The compiler knows every subtype and checks all are handled
Exhaustiveness is checked at compile time.
Where can subclasses of a sealed class be declared?Related
Answer: Only in the same library (file)
That is what makes the set known.
Why put a request id in both the log and the error message?Related
Answer: So support can find the exact log line when a user reports a problem
The user quotes the reference, and engineers search the logs for it.
From Handling errors well
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Saving data and calling APIs” before its exam: Offline-first apps and DataStore.
- Module 8 exam: Saving data and calling APIs · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Permissions, Material 3 and polish. 50 topics left in the course before the final exam.