Networking in Swift: Fetching JSON with `URLSession` and `Codable`
WALL-E spent hundreds of years alone on Earth — imagine if he could have fetched data from a server to find out where the humans went. In real apps, networking is how your code talks to the outside world: fetching movie listings, loading user profiles, or pulling down the latest Pixar release dates from an API.
You’ll learn how to make GET requests with URLSession, decode JSON into Swift types using Codable and JSONDecoder,
handle networking errors, and display fetched data in a SwiftUI view. We won’t cover advanced topics like POST requests,
authentication, or building a full networking layer — those are covered in dedicated posts.
What You’ll Learn
- What Is Networking?
- URLSession: Making Requests
- Codable: Decoding JSON
- Putting It Together: Fetch and Decode
- Displaying Remote Data in SwiftUI
- Handling Errors
- Common Mistakes
- What’s Next?
What Is Networking?
Networking is the process of sending and receiving data over the internet. When your app makes a request to a server, the server sends back a response — usually formatted as JSON (JavaScript Object Notation), a lightweight text format that both humans and computers can read.
Think of it like Marlin’s journey across the ocean in Finding Nemo. Marlin (your app) sends out a message: “Where is Nemo?” The ocean current (the internet) carries the message to Sydney (the server), and a response comes back with Nemo’s location (the JSON data).
Here’s what a typical JSON response looks like:
{
"title": "Finding Nemo",
"year": 2003,
"director": "Andrew Stanton"
}
Swift provides two key tools for networking:
- URLSession — makes HTTP requests and receives responses
- Codable — converts JSON data into Swift structs and back
URLSession: Making Requests
URLSession is Apple’s built-in API for making network requests. The simplest way to fetch data is with its
data(from:) method, which uses Swift’s async/await syntax.
Apple Docs:
URLSession— Foundation
let url = URL(string: "https://api.example.com/movies")!
let (data, response) = try await URLSession.shared.data(from: url)
Let’s break this down:
URL(string:)creates a URL from a string. The!force-unwraps it — in production code, you’d handle this more safely.URLSession.sharedis a ready-to-use session that handles most common scenarios..data(from:)makes a GET request and returns two values: the rawDatafrom the server and theURLResponsemetadata.try awaitmeans this code can fail (try) and runs asynchronously (await) — the app doesn’t freeze while waiting for the server.
If you haven’t used async/await before, think of it as telling Swift: “Start this task and come back to me when it’s
done.” Your app stays responsive while the network request happens in the background. We’ll explore async/await in depth
in Introduction to Async/Await.
Codable: Decoding JSON
Raw Data from the server isn’t useful on its own — you need to convert it into Swift types. The Codable protocol
makes this automatic.
Apple Docs:
Codable— Swift Standard Library
A Codable struct declares properties that match the JSON keys. JSONDecoder handles the conversion.
Apple Docs:
JSONDecoder— Foundation
struct Movie: Codable {
let title: String
let year: Int
let director: String
}
let decoder = JSONDecoder()
let movie = try decoder.decode(Movie.self, from: data)
print(movie.title)
Finding Nemo
JSONDecoder reads the JSON data and creates a Movie instance by matching each JSON key ("title", "year",
"director") to the struct’s properties. If a key is missing or the type doesn’t match, the decoder throws an error.
Handling Different Key Formats
APIs often use snake_case for JSON keys (release_date), but Swift conventions use camelCase (releaseDate). You
can tell the decoder to convert automatically.
struct PixarFilm: Codable {
let title: String
let releaseDate: String
let boxOffice: Int
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let film = try decoder.decode(PixarFilm.self, from: data)
With .convertFromSnakeCase, a JSON key like "release_date" maps to the Swift property releaseDate automatically.
This is how you work with structs and classes to model your API data.
Decoding Arrays
Most APIs return a list of items, not just one. To decode an array, pass [Movie].self to the decoder.
let movies = try decoder.decode([Movie].self, from: data)
print(movies.count)
5
This decodes a JSON array of movie objects into a Swift array of Movie structs.
Putting It Together: Fetch and Decode
Here’s the complete flow — make a request and decode the response — wrapped in an async function.
func fetchMovies() async throws -> [Movie] {
let url = URL(string: "https://api.example.com/pixar-movies")!
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode([Movie].self, from: data)
}
This function:
- Creates the URL
- Fetches raw data from the server
- Decodes the JSON into an array of
Moviestructs - Returns the result (or throws if anything fails)
The _ ignores the URLResponse since we only need the data for now. In production, you’d check the response’s HTTP
status code.
Displaying Remote Data in SwiftUI
To show fetched data in a SwiftUI view, call your async function from a .task modifier. This runs the async work when
the view appears and cancels it if the view disappears.
struct MovieListView: View {
@State private var movies: [Movie] = []
@State private var isLoading = true
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView("Loading movies...")
} else {
List(movies, id: \.title) { movie in
VStack(alignment: .leading) {
Text(movie.title).font(.headline)
Text("Directed by \(movie.director)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
.navigationTitle("Pixar Movies")
.task {
do {
movies = try await fetchMovies()
} catch {
print("Failed to load: \(error)")
}
isLoading = false
}
}
}
}
The .task modifier is the SwiftUI way to start async work. While isLoading is true, the user sees a spinner. Once
the data arrives, the list populates. If the request fails, we print the error and stop the loading indicator.
Handling Errors
Network requests can fail for many reasons — no internet, server errors, invalid JSON. Using
error handling with do-catch lets you respond gracefully.
struct RobustMovieListView: View {
@State private var movies: [Movie] = []
@State private var errorMessage: String?
@State private var isLoading = true
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView("Loading...")
} else if let errorMessage {
ContentUnavailableView(
"Something Went Wrong",
systemImage: "wifi.exclamationmark",
description: Text(errorMessage)
)
} else {
List(movies, id: \.title) { movie in
Text(movie.title)
}
}
}
.navigationTitle("Pixar Movies")
.task { await loadMovies() }
}
}
private func loadMovies() async {
do {
movies = try await fetchMovies()
} catch is URLError {
errorMessage = "Check your internet connection."
} catch {
errorMessage = "Couldn't load movies. Try again later."
}
isLoading = false
}
}
This view handles three states: loading, error, and success. URLError catches network-specific problems like no
internet or a timeout, while the generic catch handles everything else — like malformed JSON. ContentUnavailableView
is Apple’s built-in view for showing empty or error states.
Checking the HTTP Status Code
The server response includes a status code that tells you whether the request succeeded. A 200 means success, 404
means not found, and 500 means something broke on the server.
func fetchMovies() async throws -> [Movie] {
let url = URL(string: "https://api.example.com/pixar-movies")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decoder = JSONDecoder()
return try decoder.decode([Movie].self, from: data)
}
We cast the generic URLResponse to HTTPURLResponse to access the statusCode. If it’s not 200, we throw an error
instead of trying to decode garbage data.
Common Mistakes
Decoding on the Main Thread Without async/await
Network requests block the main thread if you don’t use async/await. This freezes your UI while waiting for the
server.
// ❌ Don't do this — blocks the main thread
let data = try Data(contentsOf: url)
let movies = try JSONDecoder().decode([Movie].self, from: data)
// ✅ Do this — runs asynchronously
let (data, _) = try await URLSession.shared.data(from: url)
let movies = try JSONDecoder().decode([Movie].self, from: data)
Always use URLSession with async/await for network requests. Data(contentsOf:) is meant for local files, not URLs.
Mismatched Codable Property Names
If your Swift property names don’t match the JSON keys and you haven’t set a key decoding strategy, the decoder will throw an error.
// JSON: { "box_office": 940000000 }
// ❌ Don't do this — property doesn't match JSON key
struct Film: Codable {
let boxOffice: Int // Expects "boxOffice" in JSON
}
// ✅ Do this — set the decoding strategy
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let film = try decoder.decode(Film.self, from: data)
Use .convertFromSnakeCase when the API uses snake_case, or define a CodingKeys enum for full control over the
mapping.
Forgetting to Handle the Error Case
If you call an async throwing function without a do-catch block, your app will crash on the first network failure.
// ❌ Don't do this — crash on any error
.task {
movies = try! await fetchMovies()
}
// ✅ Do this — handle errors gracefully
.task {
do {
movies = try await fetchMovies()
} catch {
errorMessage = error.localizedDescription
}
}
Never use try! for network code. The internet is unreliable — closures and error
handling make your app resilient.
What’s Next?
- Networking is how your app sends and receives data over the internet
URLSession.shared.data(from:)makes GET requests asynchronouslyCodablestructs model your JSON data, andJSONDecoderconverts raw data into Swift types.convertFromSnakeCasebridges API naming conventions to Swift style- The
.taskmodifier runs async work when a SwiftUI view appears - Always handle errors with
do-catch— the network is never guaranteed
Ready to dive deeper into asynchronous programming? Head over to Introduction to Async/Await to understand how Swift handles concurrent work under the hood.