Mastering Codable : Pre Codable JSON Parsing
of our Swift JSON Codable masterclass. Before Codable existed, there was JSONSerialization. Experience legacy Objective-C JSON parsing, optional casting, and understand exactly why Codable was invented.
Mastering Swift JSON Codable : The Dark Ages of JSON Parsing
Before we dive into the elegant, type-safe magic of Swift's Codable protocol in Part 1, we must take a trip back in time.
To truly appreciate a modern luxury, you must understand the suffering that preceded it.
Prior to Swift 4 (released in 2017), parsing JSON in iOS was an agonizing, error-prone, and incredibly verbose process. There was no compiler safety. There were no magically synthesized initializers. A simple typo in a string key wouldn't fail at compile-time; it would cause a catastrophic silent failure in production.
Today, in this special prequel to our Codable Masterclass, we are going to manually parse JSON the old-fashioned way using JSONSerialization. We are going to experience the "Pyramid of Doom," the nightmare of Type Casting, and the fragile architecture of the Objective-C era by fully parsing a real-world API response.
Welcome to the Dark Ages of iOS networking.
1. The Real-World API
We are going to hit a popular mock API endpoint: https://dummyjson.com/products.
This API returns a massive JSON payload containing a list of products. We want to extract a specific subset of this data. We want the total number of products, and we want to build an array of Product objects containing the ID, Title, Price, Tags, and physical Dimensions.
Here is what the JSON structure looks like:
{
"total": 194,
"skip": 0,
"limit": 30,
"products": [
{
"id": 1,
"title": "Essence Mascara Lash Princess",
"price": 9.99,
"tags": ["beauty", "mascara"],
"dimensions": {
"width": 15.14,
"height": 13.08,
"depth": 22.99
}
// ... dozens of other ignored fields ...
}
]
}
We want to map this directly into these native Swift structs:
struct Dimensions {
let width: Double
let height: Double
let depth: Double
}
struct Product {
let id: Int
let title: String
let price: Double
let tags: [String]
let dimensions: Dimensions
}
struct ProductResponse {
let total: Int
let products: [Product]
}
In modern Swift, we would conform these structs to Codable, pass the data to JSONDecoder, and be done in a single line of code.
But in 2016, we had to rely on a Foundation class called JSONSerialization. Let's build it.
2. The JSONSerialization Class
When your network request finished, URLSession handed you a block of raw bytes (Data).
The first step was to ask JSONSerialization to read those bytes and attempt to construct an Objective-C object graph (Dictionaries and Arrays).
import Foundation
func fetchAndParseProducts async {
guard let url = URL(string: "https://dummyjson.com/products") else { return }
do {
// 1. Make the actual API call to download the raw bytes
let (rawNetworkData, _) = try await URLSession.shared.data(from: url)
// 2. Ask the legacy serializer to parse the raw bytes
let jsonObject = try JSONSerialization.jsonObject(with: rawNetworkData, options: [])
// 3. The First Cast: We must force the untyped "Any" object into a Dictionary
guard let rootDict = jsonObject as? [String: Any] else {
print("Error: The root JSON was not a Dictionary!")
return
}
// This is where we start the manual JSON extraction
} catch {
print("Failed to fetch or parse JSON: \(error)")
}
}
The "Any" Problem
If you look at the type of rootDict, the values are Any. The Swift compiler has absolutely no idea if the "total" key holds an integer, a string, or an array. To do anything useful, we must perform an Optional Downcast (as?) on every single field.
3. JSON Extraction
To safely extract our ProductResponse, we must manually pull the total integer, cast the products key into an array of dictionaries ([[String: Any]]), iterate over that array, and then manually extract and cast every single nested property for every single product!
Here is how we do it:
// --- EXTRACTING THE ROOT LEVEL ---
guard let total = rootDict["total"] as? Int else {
print("Error: Missing 'total' integer at root")
return
}
guard let productsArray = rootDict["products"] as? [[String: Any]] else {
print("Error: Missing 'products' array at root")
return
}
var parsedProducts = [Product]
// --- ITERATING OVER THE ARRAY ---
for productDict in productsArray {
// 1. Extract Top-Level Primitives
guard let id = productDict["id"] as? Int,
let title = productDict["title"] as? String,
let price = productDict["price"] as? Double else {
print("Skipping malformed product (missing id, title, or price)")
continue
}
// 2. Extract String Arrays
guard let tags = productDict["tags"] as? [String] else {
print("Skipping product \(id) (missing or invalid tags array)")
continue
}
// 3. Extract Nested Objects (Dimensions)
guard let dimensionsDict = productDict["dimensions"] as? [String: Any] else {
print("Skipping product \(id) (missing dimensions object)")
continue
}
// 4. Extract Primitives from the Nested Object
guard let width = dimensionsDict["width"] as? Double,
let height = dimensionsDict["height"] as? Double,
let depth = dimensionsDict["depth"] as? Double else {
print("Skipping product \(id) (dimensions are malformed)")
continue
}
// --- BUILDING THE STRUCTS ---
// We survived! We finally have enough typed data to initialize our objects!
let parsedDimensions = Dimensions(width: width, height: height, depth: depth)
let parsedProduct = Product(
id: id,
title: title,
price: price,
tags: tags,
dimensions: parsedDimensions
)
parsedProducts.append(parsedProduct)
}
// Finally, build the root response object!
let finalResponse = ProductResponse(total: total, products: parsedProducts)
print("Successfully parsed \(finalResponse.products.count) out of \(finalResponse.total) products!")
4. The Fragility of Hardcoded Strings
Look closely at the massive block of code above. It is a disaster waiting to happen.
What happens if you accidentally type dimensionsDict["Width"] (with a capital W)?
The Swift compiler will happily compile the code. But when the app runs, the dictionary lookup will fail, the guard statement will trip, and the entire parsing operation for that product will silently fail.
You have absolutely zero compile-time safety. You are relying entirely on perfectly typing string literals. If a backend developer changes "price" to "cost", your app breaks at runtime, and the compiler won't warn you. The amount of boilerplate as? casting required for a real-world app spans hundreds of lines of code. It was a miserable experience that led to massive, bloated networking layers.
The era of manual casting is over. In next part, we will begin our true masterclass, diving deep into the Codable protocol, JSONDecoder, and how to flawlessly map complex network responses into beautiful, type-safe Swift structs!