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:
- How JSON responses are converted into Rust structs
- 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:
Deserializeallows the Serde library to convert JSON into Rust structs.Optionis used because the API may sometimes omit fields.WeatherResponseacts 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:
| Field | Meaning |
|---|---|
temperature_2m | Current temperature at 2 meters above ground |
weather_code | Numeric 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:
- Send an HTTP request to the weather API
- Receive a JSON response
- 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.