Parsing JSON Cheatsheet

1. Working with JSON where the root level is a dictionary

import Foundation

// Parsing Dictionary

//=====================================================
// JSON Data
//=====================================================

let json = """
{
 "results": [
   {
     "firstName": "John",
     "lastName": "Appleseed"
   },
  {
    "firstName": "Alex",
    "lastName": "Paul"
  }
 ]
}
""".data(using: .utf8)!

//=====================================================
// Create Model(s)
//=====================================================

// Codable: Decodable & Encodable
// Decodable: converts json data
// Encodale: converts to json data to e.g POST to a Web API

// Top Level JSON is a Dictionary
struct ResultsWrapper: Decodable {
  let results: [Contact]
}

struct Contact: Decodable {
  let firstName: String
  let lastName: String
}


//=====================================================
// decode the JSON data to our Swift model
//=====================================================

do {
  let dictionary = try JSONDecoder().decode(ResultsWrapper.self, from: json)
  let contacts = dictionary.results // [Contact]
  dump(contacts)
} catch {
  print("decoding error: \(error)")
}

/*
 ▿ 2 elements
 ▿ __lldb_expr_1.Contact
   - firstName: "John"
   - lastName: "Appleseed"
 ▿ __lldb_expr_1.Contact
   - firstName: "Alex"
   - lastName: "Paul"
*/

2. Working with JSON where the root level is an array

import Foundation

//===================================================
// JSON data
//===================================================

let json = """
[
    {
        "title": "New York",
        "location_type": "City",
        "woeid": 2459115,
        "latt_long": "40.71455,-74.007118"
    }
]
""".data(using: .utf8)!

//===================================================
// Create model(s)
//===================================================


struct City: Decodable {
  let title: String
  let locationType: String
  
  // reminder - once property names are changed
  // using CodingKeys, they must match identically to
  // the case type
  //let location_type: String
  
  let coordinate: String
  let woeid: Int
  
  //let latt_long: String
  
  private enum CodingKeys: String, CodingKey {
    case title
    case locationType = "location_type"
    case woeid
    case coordinate = "latt_long"
  }
}

//===================================================
// decode JSON to Swift objects
//===================================================

do {
  let weatherArray = try JSONDecoder().decode([City].self, from: json)
  dump(weatherArray)
} catch {
  print("decoding error: \(error)")
}

3. Working with JSON where the root level is made up of multiple dictionary objects with **(multiple keys)

import Foundation

// rare occasions you may come across some JSON like the structure below, (multiple dictionary objects):

//===================================================
// JSON data
//===================================================

let json = """
{
  "Afpak": {
    "id": 1,
    "race": "hybrid",
    "flavors": [
      "Earthy",
      "Chemical",
      "Pine"
    ],
    "effects": {
      "positive": [
        "Relaxed",
        "Hungry",
        "Happy",
        "Sleepy"
      ],
      "negative": [
        "Dizzy"
      ],
      "medical": [
        "Depression",
        "Insomnia",
        "Pain",
        "Stress",
        "Lack of Appetite"
      ]
    }
  },
  "African": {
    "id": 2,
    "race": "sativa",
    "flavors": [
      "Spicy/Herbal",
      "Pungent",
      "Earthy"
    ],
    "effects": {
      "positive": [
        "Euphoric",
        "Happy",
        "Creative",
        "Energetic",
        "Talkative"
      ],
      "negative": [
        "Dry Mouth"
      ],
      "medical": [
        "Depression",
        "Pain",
        "Stress",
        "Lack of Appetite",
        "Nausea",
        "Headache"
      ]
    }
  },
  "Afternoon Delight": {
    "id": 3,
    "race": "hybrid",
    "flavors": [
      "Pepper",
      "Flowery",
      "Pine"
    ],
    "effects": {
      "positive": [
        "Relaxed",
        "Hungry",
        "Euphoric",
        "Uplifted",
        "Tingly"
      ],
      "negative": [
        "Dizzy",
        "Dry Mouth",
        "Paranoid"
      ],
      "medical": [
        "Depression",
        "Insomnia",
        "Pain",
        "Stress",
        "Cramps",
        "Headache"
      ]
    }
  }
}
""".data(using: .utf8)!


//===================================================
// Swift Model(s)
//===================================================

struct Strain: Decodable {
  let id: Int
  let race: String
  let flavors: [String]
  let effects: [String: [String]]
}


//===================================================
// decode JSON to Swift objects
//===================================================

do {
  let dictionary = try JSONDecoder().decode([String: Strain].self, from: json)
  // use a for-loop to create [Strain] or use map {}
  var strains = [Strain]()
  for (_, value) in dictionary {
    let strain = Strain(id: value.id,
                        race: value.race,
                        flavors: value.flavors,
                        effects: value.effects)
    strains.append(strain)
  }
  dump(strains)
} catch {
  print(error)
}

4. Working with JSON where a value is heterogenous

In the JSON below postcode can be an Int or a String. In this case we make postcode a custom enum type and override init(from decoder:) to handle either case as the object is being decoded.

import Foundation


//===================================================
// JSON data
//===================================================

let jsonHeterogeneous = """
{
  "results": [{
      "gender": "male",
      "name": {
        "title": "Mr",
        "first": "Asher",
        "last": "King"
      },
      "location": {
        "street": {
          "number": 6848,
          "name": "Madras Street"
        },
        "city": "Whanganui",
        "state": "Manawatu-Wanganui",
        "country": "New Zealand",
        "postcode": 83251,
        "coordinates": {
          "latitude": "64.3366",
          "longitude": "-140.5100"
        },
        "timezone": {
          "offset": "0:00",
          "description": "Western Europe Time, London, Lisbon, Casablanca"
        }
      },
      "email": "asher.king@example.com",
      "login": {
        "uuid": "157a7e5d-6023-40bc-8b72-d391c5a20e73",
        "username": "lazycat871",
        "password": "punkin",
        "salt": "xchlaTXG",
        "md5": "1de85cd9e9fb19e4932fa2d94397f9f7",
        "sha1": "695dbe4886625b61d766320f4c759d920bd03c06",
        "sha256": "7df1eb54d38a127e7f181590f802c8ee5a6c887b141b5e90ee981eee719b9f71"
      },
      "dob": {
        "date": "1955-07-04T19:24:43.057Z",
        "age": 65
      },
      "registered": {
        "date": "2012-04-01T10:07:09.601Z",
        "age": 8
      },
      "phone": "(736)-108-4205",
      "cell": "(736)-836-4316",
      "id": {
        "name": "",
        "value": null
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/men/6.jpg",
        "medium": "https://randomuser.me/api/portraits/med/men/6.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/men/6.jpg"
      },
      "nat": "NZ"
    },
    {
      "gender": "female",
      "name": {
        "title": "Ms",
        "first": "Madison",
        "last": "Williams"
      },
      "location": {
        "street": {
          "number": 64,
          "name": "Argyle St"
        },
        "city": "Kingston",
        "state": "Ontario",
        "country": "Canada",
        "postcode": "L7J 7K7",
        "coordinates": {
          "latitude": "-80.5612",
          "longitude": "2.7939"
        },
        "timezone": {
          "offset": "+4:00",
          "description": "Abu Dhabi, Muscat, Baku, Tbilisi"
        }
      },
      "email": "madison.williams@example.com",
      "login": {
        "uuid": "b5ad5a75-2a2c-4bfb-9028-a6ef95f85068",
        "username": "lazyostrich722",
        "password": "genesis1",
        "salt": "58Mw0Gvb",
        "md5": "af2a89591d1be11120ac0de395818eb7",
        "sha1": "63c2a6c7ddf7051a56c88ca767bc1579d88472fe",
        "sha256": "509e975c1f0ef8720b6990b6922e8c30a7f589e728b001bcd6092b4a6a55b234"
      },
      "dob": {
        "date": "1977-01-11T04:47:50.049Z",
        "age": 43
      },
      "registered": {
        "date": "2003-01-06T04:41:11.111Z",
        "age": 17
      },
      "phone": "326-431-8326",
      "cell": "042-933-3343",
      "id": {
        "name": "",
        "value": null
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/women/9.jpg",
        "medium": "https://randomuser.me/api/portraits/med/women/9.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/women/9.jpg"
      },
      "nat": "CA"
    }
  ]
}
""".data(using: .utf8)!


//===================================================
// Swift Model(s)
//===================================================

struct PersonWrapper: Decodable {
  let results: [Person]
}

struct Person: Decodable {
  let gender: String
  let email: String
  let location: Location
}

struct Location: Decodable {
  let city: String
  let state: String
  let country: String
  // heterogeneious property
  //let postcode: String // Int
  let postcode: PostCode
}

enum DecodingError: Error {
  case missingValue
}

enum PostCode: Decodable {
  case int(Int)
  case string(String)
  
  init(from decoder: Decoder) throws {
    if let intValue = try? decoder.singleValueContainer().decode(Int.self) {
      self = .int(intValue)
      return
    }
    if let stringValue = try? decoder.singleValueContainer().decode(String.self) {
      self = .string(stringValue)
      return
    }
    throw DecodingError.missingValue
  }
}


//===================================================
// decode the JSON to Swift objects
//===================================================

do {
  let people = try JSONDecoder().decode(PersonWrapper.self, from: jsonHeterogeneous)
  dump(people)
  /*
   Heterogeneous JSON example
   ▿ __lldb_expr_140.PersonWrapper
     ▿ results: 2 elements
       ▿ __lldb_expr_140.Person
         - gender: "male"
         - email: "asher.king@example.com"
         ▿ location: __lldb_expr_140.Location
           - city: "Whanganui"
           - state: "Manawatu-Wanganui"
           - country: "New Zealand"
           ▿ postcode: __lldb_expr_140.PostCode.int
             - int: 83251
       ▿ __lldb_expr_140.Person
         - gender: "female"
         - email: "madison.williams@example.com"
         ▿ location: __lldb_expr_140.Location
           - city: "Kingston"
           - state: "Ontario"
           - country: "Canada"
           ▿ postcode: __lldb_expr_140.PostCode.string
             - string: "L7J 7K7"
  */
} catch {
  dump(error)
  /*
   1).
   if postcode is defined as an Int
   debugDescription: "Expected to decode Int but found a string/data instead."
   2).
   if postcode is defined as a String
   debugDescription: "Expected to decode String but found a number instead."
  */
}

More on Github

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s