LearnAI ToolsCareerPractice BuildsPlayContact
SwiftIntermediate~2 hours

Weather Lookup Tool

Parse simulated JSON weather data with Codable, safely unwrapping every optional field.

CodableOptionalsClosures

Overview

Real weather APIs never guarantee every field will be present — a sensor might not report humidity, a "feels like" figure might be omitted in mild conditions, and an alerts list only shows up when there is actually something to warn about. That is precisely the shape `Codable` is built for: a `WeatherResponse` struct where the genuinely optional fields are typed as Optionals, so Swift's `JSONDecoder` automatically treats a missing JSON key as `nil` for those fields, with no manual key-checking required.

By the end of this tutorial you will have a console application that decodes simulated JSON weather payloads into a `Codable` struct with `JSONDecoder`, wrapped in a `do`/`catch` block so a malformed payload is reported rather than silently ignored, and a closure-based formatter that safely unwraps every optional field before ever printing it — no force-unwraps, and no field that could crash the program if the data behind it happens to be absent.

What You'll Build
  • A `WeatherResponse: Codable` struct matching a realistic simulated JSON payload.
  • Optional fields (`feelsLike`, `humidity`, `alerts`) that decode to `nil` when the JSON omits them.
  • A dictionary of simulated raw JSON strings standing in for a real weather API.
  • A `fetchWeather(for:)` function using `JSONDecoder().decode(...)` inside a `do`/`catch`.
  • A closure-based `formatWeather` value that unwraps every optional with `map` and `??` before display.
  • A `readLine()`-driven lookup loop tying it all together.

Prerequisites

  • Structs — stored properties and `Codable` conformance basics.
  • Optionals — `Optional<T>` fields, `if let`, `map` on an Optional, and nil-coalescing with `??`.
  • Closures — declaring a closure with an explicit type and calling it like a function.
  • Error handling — `do`/`catch` and the `try` keyword around a throwing call.
  • Dictionaries — `[String: String]` and subscript lookup returning an Optional.

Project Structure

The program lives in a single file, `main.swift`. `WeatherResponse` is the `Codable` model matching the shape of the (simulated) API payload. `simulatedResponses` is a `[String: String]` dictionary standing in for a real network call — a genuine app would replace it with a `URLSession.shared.dataTask` request, but the decoding, error handling, and formatting logic below would not need to change at all, since all of it only depends on already having a `Data` value to decode. `fetchWeather(for:)` turns a city name into an Optional `WeatherResponse`, and `formatWeather` is a standalone closure responsible for turning that struct into safe, human-readable text.

Step 1: Define the WeatherResponse Codable Struct

Every stored property here is itself `Codable` — `String`, `Double`, `Int`, `[String]`, and Optionals of `Codable` types are all automatically `Codable` — so Swift can synthesize the entire encoding and decoding implementation with no manual `CodingKeys` or `init(from:)` needed. `feelsLike`, `humidity`, and `alerts` are Optionals specifically because a real weather provider does not always include them in every response.

// Codable is a compiler-synthesized conformance: because every stored
// property below is itself Codable (String, Double, Int, [String], and
// Optionals of Codable types), Swift generates the JSON encoding/decoding
// logic automatically — no manual keys or init(from:) needed.
struct WeatherResponse: Codable {
let city: String
let temperature: Double
let condition: String
let feelsLike: Double? // Optional: some providers omit "feels like" entirely for certain conditions
let humidity: Int? // Optional: not every sensor reports humidity
let alerts: [String]? // Optional array: omitted entirely from the payload when there are no active alerts
}

Step 2: Simulate a JSON API Response

A real app would fetch this data over the network; here the payloads are inlined as Swift multi-line strings so the project can be run and understood with no network access or API key required. Notice that Delhi's payload omits `feelsLike` and `alerts` entirely, and Bengaluru's omits `humidity` and `alerts` — `JSONDecoder` handles a missing key for an Optional property by decoding it as `nil` automatically, with no extra configuration needed.

// A real app would fetch this over the network with URLSession; here the
// payloads are inlined as Swift multi-line strings so the project can be run
// and understood without any network access or API key.
let simulatedResponses: [String: String] = [
"mumbai": """
{"city": "Mumbai", "temperature": 31.5, "condition": "Humid", "feelsLike": 36.0, "humidity": 78, "alerts": ["Heavy rain expected"]}
""",
"delhi": """
{"city": "Delhi", "temperature": 24.0, "condition": "Clear", "humidity": 40}
""",
"bengaluru": """
{"city": "Bengaluru", "temperature": 22.5, "condition": "Cloudy", "feelsLike": 22.0}
"""
]

Step 3: Decode JSON With JSONDecoder and do/catch

`decoder.decode(WeatherResponse.self, from:)` is a throwing function — it can fail if the JSON does not actually match `WeatherResponse`'s shape — so the call is wrapped in `try` inside a `do`/`catch` block rather than using `try?`, specifically so a genuine parsing failure can be logged with `catch`'s `error` value instead of silently collapsing into the same `nil` an unknown city would also produce.

func fetchWeather(for city: String) -> WeatherResponse? {
guard let json = simulatedResponses[city.lowercased()] else {
return nil // Unknown city: nothing to decode at all
}
guard let data = json.data(using: .utf8) else {
return nil // Defensive: only fails if the string somehow isn't valid UTF-8
}
do {
let decoder = JSONDecoder()
return try decoder.decode(WeatherResponse.self, from: data) // Throws if the JSON doesn't match WeatherResponse
} catch {
// A real API could send malformed data; surface the failure instead of
// silently treating a bad payload the same as an unknown city.
print("Failed to parse weather data for \(city): \(error)")
return nil
}
}
Example Usage

Click Run to see what this code prints.

Step 4: Build a Closure-Based Formatter That Unwraps Every Optional

`formatWeather` is stored in a `let` as a closure value with an explicit `(WeatherResponse) -> String` type, not written as a free function — that makes the formatting strategy itself a value that could later be swapped for a different one (a metric-vs-imperial formatter, for instance) without touching the code that calls it. Inside it, `Optional.map` runs its closure only when there is a real value and leaves `nil` as `nil`, and the trailing `??` supplies the fallback text for that `nil` case — together they unwrap every optional field with no force-unwraps anywhere in the function.

// A closure stored in a let, not a free function — this makes the
// formatting strategy a value that could be swapped out (e.g. a metric vs.
// imperial formatter) without touching the code that calls it.
let formatWeather: (WeatherResponse) -> String = { weather in
var lines: [String] = []
lines.append("Weather for \(weather.city): \(weather.condition), \(weather.temperature)°C")
// map on an Optional runs the closure only if there is a value, leaving
// nil as nil — then ?? supplies the fallback text for the nil case, so
// every branch is handled without a single force-unwrap anywhere.
let feelsLikeText = weather.feelsLike.map { "\($0)°C" } ?? "not reported"
lines.append("Feels like: \(feelsLikeText)")
let humidityText = weather.humidity.map { "\($0)%" } ?? "not reported"
lines.append("Humidity: \(humidityText)")
if let alerts = weather.alerts, !alerts.isEmpty {
lines.append("Alerts: \(alerts.joined(separator: ", "))")
} else {
lines.append("Alerts: none")
}
return lines.joined(separator: "\n")
}

Step 5: Build the Lookup Loop

The loop is deliberately simple: read a city name, check for the `"exit"` sentinel, and otherwise hand the input to `fetchWeather(for:)`. If that returns a real `WeatherResponse`, `formatWeather` from Step 4 renders it; if it returns `nil` — either because the city is not in `simulatedResponses` or because decoding failed — the loop prints a plain "not found" message instead of ever touching a field on a value that might not exist.

var running = true
while running {
print("""
===== WEATHER LOOKUP TOOL =====
Enter a city (Mumbai, Delhi, Bengaluru) or "exit" to quit:
""")
print("> ", terminator: "")
guard let input = readLine(), !input.isEmpty else {
print("Please enter a city name.")
continue
}
if input.lowercased() == "exit" {
print("Goodbye!")
running = false
continue
}
if let weather = fetchWeather(for: input) {
print(formatWeather(weather)) // Reuses the closure from Step 4 to render the result
} else {
print("No weather data found for \(input).")
}
}

Complete Code

Here is the full program assembled in the correct order, ready to save as `main.swift` and run with `swift main.swift`, or paste into a Swift Playground.

import Foundation
struct WeatherResponse: Codable {
let city: String
let temperature: Double
let condition: String
let feelsLike: Double?
let humidity: Int?
let alerts: [String]?
}
let simulatedResponses: [String: String] = [
"mumbai": """
{"city": "Mumbai", "temperature": 31.5, "condition": "Humid", "feelsLike": 36.0, "humidity": 78, "alerts": ["Heavy rain expected"]}
""",
"delhi": """
{"city": "Delhi", "temperature": 24.0, "condition": "Clear", "humidity": 40}
""",
"bengaluru": """
{"city": "Bengaluru", "temperature": 22.5, "condition": "Cloudy", "feelsLike": 22.0}
"""
]
func fetchWeather(for city: String) -> WeatherResponse? {
guard let json = simulatedResponses[city.lowercased()] else {
return nil
}
guard let data = json.data(using: .utf8) else {
return nil
}
do {
let decoder = JSONDecoder()
return try decoder.decode(WeatherResponse.self, from: data)
} catch {
print("Failed to parse weather data for \(city): \(error)")
return nil
}
}
let formatWeather: (WeatherResponse) -> String = { weather in
var lines: [String] = []
lines.append("Weather for \(weather.city): \(weather.condition), \(weather.temperature)°C")
let feelsLikeText = weather.feelsLike.map { "\($0)°C" } ?? "not reported"
lines.append("Feels like: \(feelsLikeText)")
let humidityText = weather.humidity.map { "\($0)%" } ?? "not reported"
lines.append("Humidity: \(humidityText)")
if let alerts = weather.alerts, !alerts.isEmpty {
lines.append("Alerts: \(alerts.joined(separator: ", "))")
} else {
lines.append("Alerts: none")
}
return lines.joined(separator: "\n")
}
var running = true
while running {
print("""
===== WEATHER LOOKUP TOOL =====
Enter a city (Mumbai, Delhi, Bengaluru) or "exit" to quit:
""")
print("> ", terminator: "")
guard let input = readLine(), !input.isEmpty else {
print("Please enter a city name.")
continue
}
if input.lowercased() == "exit" {
print("Goodbye!")
running = false
continue
}
if let weather = fetchWeather(for: input) {
print(formatWeather(weather))
} else {
print("No weather data found for \(input).")
}
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Swap the hardcoded dictionary for a real `URLSession.shared.dataTask` call against a live weather API.
  • Add a `forecast: [DayForecast]?` array to `WeatherResponse` and render a multi-day outlook.
  • Cache decoded `WeatherResponse` values in a `[String: WeatherResponse]` dictionary keyed by city to avoid re-decoding repeated lookups.
  • Add a second closure that converts temperature to Fahrenheit, and let the user choose a unit at startup.
  • Write unit tests with `XCTest` that feed known-good and deliberately malformed JSON into `fetchWeather` to confirm both paths behave correctly.

Summary

You built a weather lookup tool where `Codable` did the tedious work of turning JSON text into a typed Swift struct, and every field that could genuinely be missing was modeled as an Optional instead of assumed to always be present. The `do`/`catch` around `JSONDecoder().decode(...)` distinguished a real parsing failure from a simple "unknown city," and the closure-based formatter unwrapped every optional field with `map` and `??` instead of a single force-unwrap. That combination — `Codable` for parsing, Optionals for what might not exist, and closures for reusable formatting logic — is exactly how production Swift code handles data coming from outside the program, whether that is a real network response or, as here, a simulated one.