EthicsComputerCourses
Join training +91 90532 10052

Networking with URLSession & Codable

Swift · Lesson 25 of 30 Advanced 6 min read Updated

Most apps talk to a server: a train app fetches running status, a shop app loads products, a college app downloads the timetable. The data almost always travels as JSON, a text format of objects, arrays, strings, numbers and true/false values.

Swift makes this pleasant with two tools. Codable converts between JSON and your own structs with almost no code, and URLSession downloads data using the async/await you learned in async/await & Tasks. In this lesson you will decode and encode JSON, load data in a SwiftUI screen with loading and error states, and send data to a server.

Decoding JSON with Codable

Make a struct whose property names and types match the JSON keys, and declare it Codable. Then JSONDecoder does the conversion. Codable is really two protocols together: Decodable (JSON to Swift) and Encodable (Swift to JSON).

Swift
import Foundation

struct Dish: Codable {
    let name: String
    let price: Int
    let isVeg: Bool
}

let json = """
[
  {"name": "Idli", "price": 40, "isVeg": true},
  {"name": "Egg curry", "price": 90, "isVeg": false}
]
"""

do {
    let dishes = try JSONDecoder().decode([Dish].self, from: Data(json.utf8))
    for dish in dishes {
        print("\(dish.name): ₹\(dish.price)\(dish.isVeg ? " (veg)" : "")")
    }
} catch {
    print("Could not read menu: \(error)")
}
Output
Idli: ₹40 (veg)
Egg curry: ₹90

decode takes the type you want, here [Dish].self for an array of dishes, and the raw bytes as Data. It throws if the JSON does not match: a missing key, a string where a number was expected, or broken JSON. The error message names the problem key, which makes debugging much easier than hand-written parsing.

When the JSON does not match your names

Servers often use snake_case keys or short names. Set the decoder's keyDecodingStrategy to .convertFromSnakeCase, and use a CodingKeys enum to rename any other key. Make a property optional if the key may be missing.

Swift
import Foundation

struct TrainStatus: Codable {
    let trainNumber: Int
    let currentStation: String
    let delayMinutes: Int?
    let platform: String

    enum CodingKeys: String, CodingKey {
        case trainNumber, currentStation, delayMinutes
        case platform = "pf"
    }
}

let json = #"{"train_number": 22001, "current_station": "Sonipat", "pf": "2"}"#
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let status = try decoder.decode(TrainStatus.self, from: Data(json.utf8))
    print(status.trainNumber, status.currentStation, status.platform)
    print("Delay: \(status.delayMinutes ?? 0) minutes")
} catch {
    print(error)
}
Output
22001 Sonipat 2
Delay: 0 minutes

The strategy turns train_number into trainNumber before matching, and CodingKeys maps the short pf key to platform. Because delayMinutes is optional, its absence gives nil instead of an error. For dates, set decoder.dateDecodingStrategy = .iso8601 when the server sends dates like "2026-11-01T09:30:00Z". The raw string #"..."# lets the JSON keep its quotes without escaping.

Encoding Swift values to JSON

JSONEncoder goes the other way. Set outputFormatting to .sortedKeys for a predictable key order, or .prettyPrinted for readable output while debugging.

Swift
import Foundation

struct Order: Codable {
    let item: String
    let quantity: Int
}

do {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.sortedKeys]
    let data = try encoder.encode(Order(item: "Masala chai", quantity: 2))
    print(String(decoding: data, as: UTF8.self))
} catch {
    print(error)
}
Output
{"item":"Masala chai","quantity":2}

The same Order type could be decoded again from this text. Codable also works for saving small amounts of data to files, though for real app data you will use SwiftData in the next lesson.

Fetching data with URLSession

URLSession.shared.data(from:) downloads the contents of a URL and returns the data with a response. Check the HTTP status code (200 to 299 means success) before decoding. In SwiftUI, start loading with the .task modifier, which runs when the view appears and is cancelled automatically if the view disappears.

Swift
import SwiftUI

struct Todo: Codable, Identifiable {
    let id: Int
    let title: String
    let completed: Bool
}

enum LoadError: Error {
    case badStatus(Int)
}

func fetchTodos() async throws -> [Todo] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos?_limit=5")!
    let (data, response) = try await URLSession.shared.data(from: url)
    if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
        throw LoadError.badStatus(http.statusCode)
    }
    return try JSONDecoder().decode([Todo].self, from: data)
}

struct TodoListView: View {
    @State private var todos: [Todo] = []
    @State private var errorMessage: String?
    @State private var isLoading = false

    var body: some View {
        NavigationStack {
            List(todos) { todo in
                Label(todo.title, systemImage: todo.completed ? "checkmark.circle.fill" : "circle")
            }
            .overlay {
                if isLoading {
                    ProgressView("Loading...")
                } else if let errorMessage {
                    ContentUnavailableView("Could not load", systemImage: "wifi.slash",
                                           description: Text(errorMessage))
                }
            }
            .navigationTitle("Tasks")
            .task { await load() }
            .refreshable { await load() }
        }
    }

    func load() async {
        isLoading = todos.isEmpty
        defer { isLoading = false }
        do {
            todos = try await fetchTodos()
            errorMessage = nil
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

When the screen opens, a spinning indicator with "Loading..." appears in the middle. A moment later it is replaced by a list of five tasks from the free JSONPlaceholder test API, each with an empty circle or a filled tick depending on completed. Pulling the list down shows the refresh spinner and loads again (.refreshable). With the phone in flight mode, the screen instead shows a Wi-Fi-off icon, "Could not load" and the system's error message. The force unwrap in URL(string:)! is acceptable here because the address is a fixed string you typed and tested.

Sending data with a POST request

To send data, build a URLRequest, set its method, headers and body, and call data(for:).

Swift
func placeOrder(_ order: Order) async throws -> Int {
    var request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(order)

    let (_, response) = try await URLSession.shared.data(for: request)
    return (response as? HTTPURLResponse)?.statusCode ?? 0
}

Calling try await placeOrder(Order(item: "Samosa", quantity: 4)) sends the JSON to the test server, which pretends to save it and answers with status 201 ("Created"). Real APIs usually also need an authentication header; never hard-code secret keys inside an app, because anyone can extract them from the app file.

Common mistakes

  • Property names that do not match JSON keys. Use keyDecodingStrategy or CodingKeys.
  • Non-optional properties for keys that can be missing. Decoding then fails for the whole response.
  • Ignoring the HTTP status code. A 404 page is still "data"; check the status before decoding.
  • Loading in body or init. Use .task { } so loading starts once and is cancelled correctly.

Key points

  • Codable structs plus JSONDecoder/JSONEncoder convert JSON with little code.
  • .convertFromSnakeCase, CodingKeys and optional properties handle mismatched or missing keys.
  • try await URLSession.shared.data(from:) downloads data; check HTTPURLResponse.statusCode.
  • In SwiftUI, load with .task, show ProgressView while loading, and offer .refreshable.
  • URLRequest with httpMethod, headers and httpBody sends data; use HTTPS only.

Exercise

A weather server for Pune returns {"city": "Pune", "temp_c": 31.5, "humidity": 62, "alerts": ["Heat advisory"]}. Write a Codable struct for it using .convertFromSnakeCase, decode the JSON from a string, and print the city, temperature and the number of alerts. Make alerts optional so that responses without alerts also decode.

Show solution
Swift
import Foundation

struct Weather: Codable {
    let city: String
    let tempC: Double
    let humidity: Int
    let alerts: [String]?
}

let json = #"{"city": "Pune", "temp_c": 31.5, "humidity": 62, "alerts": ["Heat advisory"]}"#
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let weather = try decoder.decode(Weather.self, from: Data(json.utf8))
    print(weather.city, weather.tempC)
    print("Alerts: \(weather.alerts?.count ?? 0)")
} catch {
    print(error)
}
Output
Pune 31.5
Alerts: 1

temp_c becomes tempC through the decoding strategy. Because alerts is optional, a response without that key decodes with alerts set to nil, and ?? 0 prints zero alerts.

Quick Quiz

1. What does Codable combine?

2. A JSON key may be missing. How should the matching property be declared?

3. Where should a SwiftUI view start an async download?

4. Why should apps use https:// URLs?

Learn Swift with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call