Mastering Codable : Pre Codable JSON Parsing

September 01, 2026

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!