Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JSON Models and Error Handling

The Weather App communicates with the Open-Meteo API to retrieve weather information. The API returns data in JSON format, which must be converted into structured Rust types before the application can use it.

At the same time, network requests and external APIs may fail. Therefore, the Weather App must also implement proper error handling to avoid crashes and provide meaningful feedback to the user.

This chapter explains:

  1. How JSON responses are converted into Rust structs
  2. How the application safely handles errors during API requests

Mapping JSON Data to Rust Structures

When the Weather API returns data, the response looks conceptually like this:

{
  "current": {
    "temperature_2m": 6.3,
    "weather_code": 53
  },
  "daily": {
    "time": ["2026-03-13", "2026-03-14"],
    "temperature_2m_max": [11.8, 5.4],
    "temperature_2m_min": [5.4, 4.0],
    "weather_code": [61, 51]
  }
}

Rust cannot directly work with this raw JSON text. Instead, we define Rust structs that mirror the JSON structure.

This process is called data modeling.

Weather Response Model

The top-level JSON response is mapped into the following Rust structure:

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct WeatherResponse {
    current: Option<CurrentWeather>,
    daily: Option<DailyWeather>,
}
}

Important details:

  • Deserialize allows the Serde library to convert JSON into Rust structs.
  • Option is used because the API may sometimes omit fields.
  • WeatherResponse acts as the container for all weather data.

Current Weather Model

The current weather information is stored in a separate struct.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct CurrentWeather {
    temperature_2m: f64,
    weather_code: i32,
}
}

Field explanation:

FieldMeaning
temperature_2mCurrent temperature at 2 meters above ground
weather_codeNumeric code representing the weather condition

The weather code is later converted into a human-readable description and icon in the user interface.

Daily Forecast Model

The seven-day forecast is represented using arrays.

#![allow(unused)]
fn main() {
#[derive(Debug, Deserialize)]
struct DailyWeather {
    time: Vec<String>,
    temperature_2m_max: Vec<f64>,
    temperature_2m_min: Vec<f64>,
    weather_code: Vec<i32>,
}
}

Each index represents one forecast day.

For example:

time[0]
temperature_2m_max[0]
temperature_2m_min[0]
weather_code[0]

Together these values describe the first day in the forecast.

This design allows the application to loop through the arrays and create the forecast rows displayed in the UI.

Converting JSON into Rust Objects

After defining the models, the API response can be automatically converted using reqwest and serde.

A simplified version of the code is:

#![allow(unused)]
fn main() {
let w: WeatherResponse = reqwest::blocking::get(&weather_url)
    .map_err(|e| format!("Weather request failed: {e}"))?
    .json()
    .map_err(|e| format!("Weather JSON failed: {e}"))?;
}

This performs several steps:

  1. Send an HTTP request to the weather API
  2. Receive a JSON response
  3. Convert the JSON into WeatherResponse

After conversion, the application can safely access:

#![allow(unused)]
fn main() {
w.current
w.daily
}

No manual JSON parsing is required.

Why Option Is Important

Notice that the fields inside WeatherResponse are wrapped in Option.

#![allow(unused)]
fn main() {
current: Option<CurrentWeather>,
daily: Option<DailyWeather>,
}

This is important because:

  • APIs may return incomplete responses
  • network failures may produce partial data
  • certain fields might be missing

Using Option prevents the application from crashing and allows missing values to be handled safely.

Error Handling in the Weather App

External API requests are not always successful. Errors may occur when:

  • the user enters an invalid city name
  • the internet connection fails
  • the API returns incomplete data

The Weather App handles these situations using Rust’s Result type.

Returning Result from Fetch Functions

The weather fetch function returns a Result:

#![allow(unused)]
fn main() {
fn fetch_open_meteo(city: &str)
    -> Result<(String, f64, i32, Vec<(String, f64, f64, i32)>), String>
}

This means:

  • Ok(...) → weather data was successfully retrieved
  • Err(String) → an error occurred

Using Result forces the program to handle failures explicitly.

Handling Invalid Input

Before sending a request, the application checks whether the user entered a valid city.

#![allow(unused)]
fn main() {
if q.is_empty() {
    return Err("Empty city".into());
}
}

If the input is empty, the function immediately returns an error instead of sending an invalid API request.

Handling Missing Cities

If the geocoding API does not return a city result, the application handles it safely.

#![allow(unused)]
fn main() {
let city0 = geo.get(0)
    .cloned()
    .ok_or_else(|| "City not found".to_string())?;
}

If no matching city exists, the function returns an error rather than accessing invalid data.

This prevents crashes caused by invalid indexing.

Handling Network Errors

The HTTP request itself may fail.

For example, the internet connection might be unavailable.

The following code converts request errors into readable messages:

#![allow(unused)]
fn main() {
reqwest::blocking::get(&weather_url)
    .map_err(|e| format!("Weather request failed: {e}"))?
}

The ? operator immediately returns the error if the request fails.

This keeps the code concise and easy to read.

Handling Errors in the UI

The Weather App performs API requests in a background thread. When the request finishes, the UI checks the result.

#![allow(unused)]
fn main() {
Ok(Err(e)) => {
    show_error(e);
}
}

The UI then displays the message to the user.

For example:

#![allow(unused)]
fn main() {
now_temp.set_text(&msg);
forecast_label.set_visible(false);
forecast_list.set_visible(false);
}

This ensures:

  • the user receives a clear error message
  • outdated forecast data is removed
  • the interface remains consistent

The application continues running instead of crashing.

Handling “City Not Found” Separately

The application also detects specific errors.

#![allow(unused)]
fn main() {
if e.to_lowercase().contains("city not found") {
    show_not_found();
}
}

This allows the UI to display a more user-friendly message when a city does not exist.

Different errors can therefore produce different UI responses.