Skip to content

Intermediate: Android basics · Saving data and calling APIs · topic 33 of 50 · 30 min

JSON models and handling errors

Explanation

kotlinx.serialization turns JSON into @Serializable data classes; @SerialName maps different JSON names, default values cover missing fields, and ignoreUnknownKeys keeps old app versions working when the server adds fields. Network calls fail (no internet, timeouts, server errors), so catch IOException and HttpException and expose Loading, Success and Error states with a sealed interface, plus a Retry button.

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.

json
{
  "order_id": 9081,
  "status": "SHIPPED",
  "items": [ { "name": "Kurta", "qty": 1 } ],
  "coupon": null,
  "eta_days": 3
}
A typical API response.
kotlin
@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)
@SerialName maps snake_case JSON to camelCase Kotlin. Nullable types and defaults make fields optional.

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

FailureTypical exceptionWhat to show
No internet, DNS failure, timeoutIOException (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 modelSerializationExceptionLog 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

kotlin
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()})")
            }
        }
    }
}
try is an expression in Kotlin, so its result can be assigned directly.
kotlin
@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") }
        }
    }
}
Every state has a clear screen. The compiler checks all three are handled.
Think of it like this: A good app handles network errors like a good receptionist handles a busy phone line: "The line is busy, shall I try again?" rather than hanging up on you. Loading, Success and Error are the three things a caller can hear.

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

@Serializable data class OrderDto(...)

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.

kotlin
@Serializable
data class ArrivalDto(@SerialName("route_no") val routeNo: String, @SerialName("eta_min") val etaMin: Int)

fun ArrivalDto.toLabel() = "Route $routeNo in $etaMin min"
An extension function maps the DTO to text for the UI.

Practice (0/3 done)

Task 1 · Question

The server adds a new JSON field your class does not have. Your Json has ignoreUnknownKeys = true. What happens?

Task 2 · Question

Which exception usually means "no internet" when calling Retrofit?

Task 3 · Practice

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

Question 1

1.What does @SerialName("eta_days") do?

Question 2

2.How do you make a JSON field optional in the model?

Question 3

3.Why is catching Exception broadly risky in coroutines?

Question 4

4.What is a DTO?

Read more (free)

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.

From Data, enum and sealed classes, and objects

Where can subclasses of a sealed class be declared?Related

Answer: Only in the same library (file)

That is what makes the set known.

From Dart 3: enums, records, patterns and sealed classes

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. 1 more topic in “Saving data and calling APIs” before its exam: Offline-first apps and DataStore.
  2. Module 8 exam: Saving data and calling APIs · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Permissions, Material 3 and polish. 50 topics left in the course before the final exam.

← Previous: Networking with Retrofit