Parsing JSON in Swift 4
June 07, 2017 - Swift 4.0

Swift 4 includes a new way to generate & parse JSON using the Codable protocol. It’ll get rid of some boilerplate, especially when the objects or structs in our code have a similar structure to the JSON that we use to talk to a web service. In many cases you’ll be able to avoid writing any code that explicitly parses or generates JSON, even if your Swift structs don’t exactly match the JSON structure. That means no more long, ugly toJSON() or init?(json: [String: Any]) functions.

Codable can also replace use of NSCoding when we want to serialize objects to write them to a file and read them back again. It can work with plists as easily as JSON and you can write your own custom encoders & decoders for different formats.

Today let’s look at the simple case of converting an object or struct in our code to & from JSON. Then we’ll see how it might be more complicated when the JSON doesn’t match the objects & structs that we’re using in our code. We’ll use structs below but everything is the same if you need to use classes.

Two devs working together
(image by #WOCinTech Chat)

This tutorial uses Swift 4.0. The easiest way to use Swift 4 is to download the Xcode 9 beta. You can install it next to Xcode 8.

Like in previous tutorials, we’ll use the super handy JSONPlaceholder API:

“JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders but for web developers.”

JSONPlaceholder has a handful of resources similar to what you’ll find in a lot of apps: users, posts, photos, albums, … We’ll use Todo and User items today.

The JSON structure of a Todo looks like:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

So let’s create a struct to represent that:

struct Todo: Codable {
  var title: String
  var id: Int?
  var userId: Int
  var completed: Int
}

When we create a Todo in the app it won’t have an id until it’s saved to the server so that property is optional.

Convert a Single Item to JSON

Before we had Codable, we’d have to parse the JSON ourselves. Let’s looks at how that worked.

For fetching objects that could be done by creating an initializer that tries to create an object from JSON:

struct Todo {
  // ...

  init?(json: [String: Any]) {
    guard let title = json["title"] as? String,
      let id = json["id"] as? Int,
      let userId = json["userId"] as? Int,
      let completed = json["completed"] as? Int else {
        return nil
    }
    self.title = title
    self.userId = userId
    self.completed = completed
    self.id = id
  }
}

For convenience, let’s also make a function that will give us the URL to hit for this call based on the id of the Todo that we want to retrieve:

struct Todo {
  // ...

  static func endpointForID(_ id: Int) -> String {
    return "https://jsonplaceholder.typicode.com/todos/\(id)"
  }
}

And define some error cases so we can return something informative if we run into issues:

enum BackendError: Error {
  case urlError(reason: String)
  case objectSerialization(reason: String)
}

Then we can use URLSession to make that API call, check for errors, parse the JSON, and return the Todo:

static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
  // set up URLRequest with URL
  let endpoint = Todo.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  // Make request
  let session = URLSession.shared
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    // handle response to request
    // check for error
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    // make sure we got data in the response
    guard let responseData = data else {
      print("Error: did not receive data")
      let error = BackendError.objectSerialization(reason: "No data in response")
      completionHandler(nil, error)
      return
    }
    
    // parse the result as JSON
    // then create a Todo from the JSON
    do {
      if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
        let todo = Todo(json: todoJSON) {
        // created a TODO object
        completionHandler(todo, nil)
      } else {
        // couldn't create a todo object from the JSON
        let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
        completionHandler(nil, error)
      }
    } catch {
      // error trying to convert the data to JSON using JSONSerialization.jsonObject
      completionHandler(nil, error)
      return
    }
  })
  task.resume()
}

To call that function and check the results:

func getTodo(_ idNumber: Int) {
  Todo.todoByIDOld(idNumber, completionHandler: { (todo, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print(error)
      return
    }
    guard let todo = todo else {
      print("error getting first todo: result is nil")
      return
    }
    // success :)
    debugPrint(todo)
    print(todo.title)
  })
}

Now, what would that look like in Swift 4? We still need to create the URL and make sure we got data (and no error) from the network call. We’ll can replace this chunk of code with something more concise and delete that JSON-based initializer:

do {
  if let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
    let todo = Todo(json: todoJSON) {
    // created a TODO object
    completionHandler(todo, nil)
  } else {
    // couldn't create a todo object from the JSON
    let error = BackendError.objectSerialization(reason: "Couldn't create a todo object from the JSON")
    completionHandler(nil, error)
  }
} catch {
  // error trying to convert the data to JSON using JSONSerialization.jsonObject
  completionHandler(nil, error)
  return
}

Instead of using JSONSerialization.jsonObject then our initializer: let todo = Todo(json: todoJSON), we’ll use the Codable protocol to do it in one step.

First, we need to declare that Todo is Codable:

struct Todo: Codable {
  // ...
}

Then we can change that JSON parsing code:

static func todoByID(_ id: Int, completionHandler: @escaping (Todo?, Error?) -> Void) {
  // ...
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    guard let responseData = data else {
        // ...
    }
    guard error == nil else {
      // ...
    }
    
    let decoder = JSONDecoder()
    do {
      let todo = try decoder.decode(Todo.self, from: responseData)
      completionHandler(todo, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  })
  task.resume()
}

Here’s the new stuff:


let decoder = JSONDecoder()
do {
  let todo = try decoder.decode(Todo.self, from: responseData)
  completionHandler(todo, nil)
} catch {
  print("error trying to convert data to JSON")
  print(error)
  completionHandler(nil, error)
}

First we create a JSONDecoder(). Then we try to use it to decode the data that we got from the network call. Finally, if it succeeds we hand back the Todo to the caller using the completion handler. If it fails then we hand back an error instead.

To use the decoder we need to tell it what the class is: Todo.self and what data to parse:

let todo = try decoder.decode(Todo.self, from: responseData)

Which is a little nicer than the previous version:

let todoJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]
let todo = Todo(json: todoJSON)

But also meant that we didn’t have to write an initializer that creates a Todo from JSON. Extracting all of the elements from the JSON and setting the properties of the Todo was done for us. We can now delete the init?(json: [String: Any]) function.

Convert an Array of Items to JSON

Similarly, we can easily fetch all of the Todos from https://jsonplaceholder.typicode.com/todos.

Add the URL string to we can refer to it:

struct Todo: Codable {
  // ...
  static func endpointForTodos() -> String {
    return "https://jsonplaceholder.typicode.com/todos/"
  }
}

And create a new function that’s similar to todoById(_:):

static func allTodos(completionHandler: @escaping ([Todo]?, Error?) -> Void) {
  let endpoint = Todo.endpointForTodos()
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest) {
    (data, response, error) in
    guard let responseData = data else {
      print("Error: did not receive data")
      completionHandler(nil, error)
      return
    }
    guard error == nil else {
      completionHandler(nil, error)
      return
    }
    
    let decoder = JSONDecoder()
    do {
      let todos = try decoder.decode([Todo].self, from: responseData)
      completionHandler(todos, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  }
  task.resume()
}

The JSON handling is almost the same as the previous function:

let decoder = JSONDecoder()
do {
  let todos = try decoder.decode([Todo].self, from: responseData)
  completionHandler(todos, nil)
} catch {
  print("error trying to convert data to JSON")
  print(error)
  completionHandler(nil, error)
}

The difference is the type passed to decoder.decode. Instead of a single Todo it’s an array of Todos: [Todo].self.

Calling that function is pretty similar too, except you’ll get an array of Todo items in the completion handler instead of just one:

func getAllTodos() {
  Todo.allTodos { (todos, error) in
    if let error = error {
      // got an error in getting the data
      print(error)
      return
    }
    guard let todos = todos else {
      print("error getting all todos: result is nil")
      return
    }
    // success :)
    debugPrint(todos)
    print(todos.count)
  }
}

That’s because the completion handler is declared like completionHandler: @escaping ([Todo]?, Error?) -> Void.

Generate JSON from an Item

The Codable protocol is made up of 2 protocols: Encodable as well as Decodable. So far we’ve only used Decodable to parse JSON. Encodable can make it easier to generate JSON too.

Here’s an example of saving a Todo to the web service without using Codable:

func save(completionHandler: @escaping (Error?) -> Void) {
  let todosEndpoint = Todo.endpointForTodos()
  guard let todosURL = URL(string: todosEndpoint) else {
    let error = BackendError.urlError(reason: "Could not create URL")
    completionHandler(error)
    return
  }
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let newTodoAsJSON = self.toJSON()
  
  do {
    let jsonTodo = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])
    todosUrlRequest.httpBody = jsonTodo
  } catch {
    let error = BackendError.objectSerialization(reason: "Could not create JSON from Todo")
    completionHandler(error)
    return
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    guard error == nil else {
      completionHandler(error!)
      return
    }
    
    // if we didn't get an error then it succeeded
    completionHandler(nil)
  })
  task.resume()
}

To make that work we need to add a function to convert the Todo to JSON:

struct Todo {
  // ...
  func toJSON() -> [String: Any] {
    var json = [String: Any]()
    json["title"] = title
    json["userId"] = userId
    json["completed"] = completed
    if let id = id {
      json["id"] = id
    }
    return json
  }
}

Now in Swift 4 we can avoid writing functions like toJSON() by using an JSONEncoder instead of JSONSerialization:

func save(completionHandler: @escaping (Error?) -> Void) {
  // ...
  
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let encoder = JSONEncoder()
  do {
    let newTodoAsJSON = try encoder.encode(self)
    todosUrlRequest.httpBody = newTodoAsJSON
  } catch {
    print(error)
    completionHandler(error)
  }
  
  // ...
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    // ...
  })
  task.resume()
}

The old version isn’t much longer to call:


let newTodoAsJSON = try JSONSerialization.data(withJSONObject: newTodoAsJSON, options: [])

Compared to:


let newTodoAsJSON = try encoder.encode(self)

But being able to delete the toJSON() function is nice.

Nested JSON Objects

What if your JSON includes objects inside of objects? For example, a User from the JSON Placeholder API has an address and a company within it:

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

Let’s try to parse a few fields from the user fields and a few from inside the address object. First we’ll create Codable structs for both User and Address:

struct Address: Codable {
  let city: String
  let zipcode: String
}

struct User: Codable {
  let id: Int?
  let name: String
  let email: String
  let address: Address
  
  // MARK: URLs
  static func endpointForID(_ id: Int) -> String {
    return "https://jsonplaceholder.typicode.com/users/\(id)"
  }
}

Then we’ll set up a function to get a user:

static func userByID(_ id: Int, completionHandler: @escaping (User?, Error?) -> Void) {
  let endpoint = User.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    print("Error: cannot create URL")
    let error = BackendError.urlError(reason: "Could not create URL")
    completionHandler(nil, error)
    return
  }
  let urlRequest = URLRequest(url: url)
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest, completionHandler: {
    (data, response, error) in
    guard let responseData = data else {
      print("Error: did not receive data")
      completionHandler(nil, error)
      return
    }
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    
    let decoder = JSONDecoder()
    do {
      let user = try decoder.decode(User.self, from: responseData)
      completionHandler(user, nil)
    } catch {
      print("error trying to convert data to JSON")
      print(error)
      completionHandler(nil, error)
    }
  })
  task.resume()
}

The bit that does the JSON encoding is:

let user = try decoder.decode(User.self, from: responseData)

Then we can call that function and try to print out the user and address:

func getUser(_ idNumber: Int) {
  User.userByID(idNumber, completionHandler: { (user, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print("error calling POST on /todos")
      print(error)
      return
    }
    guard let user = user else {
      print("error getting user: result is nil")
      return
    }
    // success :)
    debugPrint(user)
    print(user.name)
    print(user.address.city)
  })
}

It works! Here’s what that prints out:

grok101.User(id: Optional(1), name: "Leanne Graham", email: "Sincere@april.biz", address: grok101.Address(city: "Gwenborough", zipcode: "92998-3874"))
Leanne Graham
Gwenborough

That example shows us two things:

  1. To parse nested JSON just create properties with Codable items for the nested JSON objects

  2. We don’t have to parse every element in the JSON. We can pick and choose just the elements that we want to parse by only creating properties for those items in our Codable item.

What if the JSON Doesn’t Match My Struct / Object?

Codable works almost magically when the JSON matches the object. Sometimes the JSON won’t exactly match your Swift declaration. So how can we handle that?

Different Property Names

The simplest case is that the property names don’t match. Suppose we wanted to use a more verbose name for the id property like serverId:

struct Todo: Codable {
  var title: String
  var serverId: Int?
  var userId: Int
  var completed: Int
}

Now if we try to run the todoById(:_) function it’ll work but we’ll end up without a value for the serverId:

Todo(title: "delectus aut autem", serverId: nil, userId: 1, completed: 0)

Since serverId is optional the parsing won’t fail but it won’t be right either. Fortunately the Codable protocol lets us specify the json key that should match up with each property using enum CodingKeys:

enum CodingKeys: String, CodingKey {
  case title
  case serverId = "id"
  case userId
  case completed
}

In CodingKeys we include all of the keys to be parsed from the JSON. If the name of the property matches the json key then we only need to include case propertyName. If they don’t match then we need to include both the property name and the json key: case propertyName = "json_key". If they all match up then the default version of CodingKeys will be automatically created and we don’t need to add it in our code.

Using case serverId = "id" will fix our JSON parsing so that the serverId gets correctly parsed:

Todo(title: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)

What if the property whose name didn’t match the JSON wasn’t optional? Let’s try changing one of the non-optional property names:

struct Todo: Codable {
  var displayTitle: String
  var serverId: Int?
  var userId: Int
  var completed: Int
}

We’ve changed title to displayTitle.

If we don’t use CodingKeys then we’ll get an error when we call try decoder.decode(Todo.self, from: responseData):

keyNotFound(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.(CodingKeys in _6B02657B511E8A9934EF1ACB6A92D55E).displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))

Which is telling us that it can’t find the JSON key to match up with the non-optional displayTitle property.

Let’s try using CodingKeys to handle this issue:

enum CodingKeys: String, CodingKey {
  case displayTitle = "title"
  case serverId = "id"
  case userId
  case completed
}

Success! That works:

Todo(displayTitle: "delectus aut autem", serverId: Optional(1), userId: 1, completed: 0)

The same CodingKeys will work with the functions to get all the Todo items and to save a new Todo so we don’t need to change anything else to support those API calls.

Parsing a Different Structure

Sometimes the item you want in your Swift code has a different structure than the JSON you’re getting. You might have to dig through a bit chunk of JSON to get a few properties that you want or you might have an item that’s split up between a few elements in the JSON.

So far we’ve been ignoring the result that we get when we create a new Todo. This what it looks like:

{
  "{\"title\":\"First todo\",\"userId\":1,\"completed\":0}": "",
  "id": 201
}

There are 2 elements in the JSON: what we sent (oddly as a key) and the id for the newly created Todo. What we really want is the new id which we could add to our Todo. First let’s update the functions to parse & expect a Todo.

For the calling function, we’ll add a second parameter to the completion handler:

func saveNewTodo() {
  let newTodo = Todo(displayTitle: "First todo", serverId: nil, userId: 1, completed: 0)
  newTodo.save { (todo, error) in
    if let error = error {
      // got an error in getting the data, need to handle it
      print("error calling POST on /todos")
      print(error)
      return
    }
    guard let todo = todo else {
      print("did not get todo in response from creating todo")
      return
    }
    // success!
    print("Saved new todo")
    debugPrint(todo)
  }
}

For the save() function, we also need to add that second parameter to the completion handler:

func save(completionHandler: @escaping (Todo?, Error?) -> Void)

Update all the calls to the completion handler to use 2 parameters:

func save(completionHandler: @escaping (Todo?, Error?) -> Void) {
  let todosEndpoint = Todo.endpointForTodos()
  guard let todosURL = URL(string: todosEndpoint) else {
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(nil, error)
    return
  }
  var todosUrlRequest = URLRequest(url: todosURL)
  todosUrlRequest.httpMethod = "POST"
  
  let encoder = JSONEncoder()
  do {
    let newTodoAsJSON = try encoder.encode(self)
    todosUrlRequest.httpBody = newTodoAsJSON
    // See if it's right
    if let bodyData = todosUrlRequest.httpBody {
      print(String(data: bodyData, encoding: .utf8) ?? "no body data")
    }
  } catch {
    print(error)
    completionHandler(nil, error)
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: todosUrlRequest, completionHandler: {
    (data, response, error) in
    guard error == nil else {
      completionHandler(nil, error!)
      return
    }
    // TODO: parse response
    completionHandler(nil, nil)
  })
  task.resume()
}

And try to parse the response:

let task = session.dataTask(with: todosUrlRequest, completionHandler: {
  (data, response, error) in
  guard error == nil else {
    let error = error!
    completionHandler(nil, error)
    return
  }
  guard let responseData = data else {
    let error = BackendError.objectSerialization(reason: "No data in response")
    completionHandler(nil, error)
    return
  }
  
  let decoder = JSONDecoder()
  do {
    let todo = try decoder.decode(Todo.self, from: responseData)
    completionHandler(todo, nil)
  } catch {
    print("error parsing response from POST on /todos")
    print(error)
    completionHandler(nil, error)
  }
})
task.resume()

But that won’t work because the JSON isn’t in the format that it expects for a Todo:

keyNotFound(grok101.Todo.CodingKeys.displayTitle,
Swift.DecodingError.Context(codingPath: [Optional(grok101.Todo.CodingKeys.displayTitle)],
debugDescription: "Key not found when expecting non-optional type String for coding key \"displayTitle\""))

How can we tell the decoder to parse that JSON? The simplest way is to create a new type that mirrors that JSON then create Todo from that item.

Since we only need the id we’ll just pull that out from the JSON to define our new struct that encapsulates the response from creating a Todo:

struct CreateTodoResult: Codable {
  var id: Int?
}

We’ll specify that we’re parsing a CreateTodoResult then get the id from it and add it to the Todo before returning it:

let decoder = JSONDecoder()
do {
  let createResult = try decoder.decode(CreateTodoResult.self, from: responseData)
  var todo = self
  todo.serverId = createResult.id
  completionHandler(todo, nil)
} catch {
  print("error parsing response from POST on /todos")
  print(error)
  completionHandler(nil, error)
}

If you can’t easily generate the structure to match the JSON and work with that, you can override the decode function. For example, if we didn’t want the user’s address to be part of a nested struct:

struct User: Codable {
  let id: Int?
  let name: String
  let email: String
  let city: String
  let zipcode: String

  // ...
}

To handle the nested structure in the JSON (but not in our struct), we define the CodingKeys including one for address. Then we add another enumc called AddressKeys that’s similar to CodingKeys and has the keys that we want to parse inside of the Address JSON object.

struct User: Decodable {
  // ...
  
  enum CodingKeys: String, CodingKey {
    case id
    case name
    case email
    case address
  }
  
  enum AddressKeys: String, CodingKey {
    case city
    case zipcode
  }
 
  // ...
}

Now we can override the initializer that’s used by the decoder when it creates a User from JSON. That’s where we’ll be able to write our own code to map the values from the JSON into our struct’s properties. Place this initializer in an extension to that you’ll still get the default memberwise initializer (otherwise we won’t be able to call self.init(displayTitle: title, serverId: serverId, userId: userId, completed: completed) without writing our own version of that function):

extension User {
  public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    let id = try values.decodeIfPresent(Int.self, forKey: .id)
    let name = try values.decode(String.self, forKey: .name)
    let email = try values.decode(String.self, forKey: .email)
    
    let addressValues = try values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
    let city = try addressValues.decode(String.self, forKey: .city)
    let zipcode = try addressValues.decode(String.self, forKey: .zipcode)
    
    self.init(id: id, name: name, email: email, city: city, zipcode: zipcode)
  }
}

decoder.container(keyedBy: CodingKeys.self) will look at the JSON and try to pull out each element using the CodingKeys. Then for each property do something like values.decodeIfPresent(Int.self, forKey: .id) to try to extract that value. The enums for the keys are based on the CodingKeys so you’ll have one for each property.

For the elements within the address key, we have to get the container based on that key: values.nestedContainer(keyedBy: AddressKeys.self, forKey: .address). Then we can extract the contents like addressValues.decode(String.self, forKey: .city).

Sending a Different Structure

To send a different structure we can implement a custom version of the encode function. For example, we might want to just send one parameter to update using a PATCH request. Suppose we wanted to update the user’s email address.

Change the declaration from let to var so we can change it:

struct User: Codable {
  let id: Int?
  let name: String
  var email: String
  let city: String
  let zipcode: String
  // ...
}

And add the encode function to an extension. Here’s how to include only the email parameter:

extension User {
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(email, forKey: .email)
  }
}

Then we can take a user, update their email address locally, and send it to the server:

var editedUser = user
editedUser.email = "a@aol.com"
editedUser.update() {
  error in
  print(error ?? "no error")
}

And create that update() function to send the PATCH request:


func update(completionHandler: @escaping (Error?) -> Void) {
  guard let id = self.id else {
    let error = BackendError.urlError(reason: "No ID provided for PATCH")
    completionHandler(error)
    return
  }
  let endpoint = User.endpointForID(id)
  guard let url = URL(string: endpoint) else {
    let error = BackendError.urlError(reason: "Could not construct URL")
    completionHandler(error)
    return
  }
  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "PATCH"
  var headers = urlRequest.allHTTPHeaderFields ?? [:]
  headers["Content-Type"] = "application/json"
  urlRequest.allHTTPHeaderFields = headers
  
  let encoder = JSONEncoder()
  do {
    let asJSON = try encoder.encode(self)
    urlRequest.httpBody = asJSON
    // See if it's right
    if let bodyData = urlRequest.httpBody {
      print(String(data: bodyData, encoding: .utf8) ?? "no body data")
    }
  } catch {
    print(error)
    completionHandler(error)
  }
  
  let session = URLSession.shared
  
  let task = session.dataTask(with: urlRequest) {
    (data, response, error) in
    guard error == nil else {
      let error = error!
      completionHandler(error)
      return
    }
    guard let responseData = data else {
      let error = BackendError.objectSerialization(reason: "No data in response")
      completionHandler(error)
      return
    }
    
    print(String(data: responseData, encoding: .utf8) ?? "No response data as string")
    completionHandler(nil)
  }
  task.resume()
}

It’s nearly the same as the save() function except the HTTP method and adding a “Content-Type: application/json” header:

urlRequest.httpMethod = "PATCH"

var headers = urlRequest.allHTTPHeaderFields ?? [:] // create empty dictionary if headers is nil
headers["Content-Type"] = "application/json"
urlRequest.allHTTPHeaderFields = headers

Before sending the request we can check to make sure the body is set properly:

if let bodyData = urlRequest.httpBody {
  print(String(data: bodyData, encoding: .utf8) ?? "no body data")
}

Which prints what we expect:

{"email":"a@aol.com"}

And after the network call is done, we can print the response body to check that the email address is changed:

guard let responseData = data else {
  let error = BackendError.objectSerialization(reason: "No data in response")
  completionHandler(error)
  return
}

print(String(data: responseData, encoding: .utf8) ?? "No response data as string")

Which prints:

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "a@aol.com",
  // ...
}

And that email address has been successfully changed :)

Some Warnings

Since your JSON parsing depends on the names of the properties make sure you don’t rename them (unless the JSON changes too). This would be a good thing to write tests for so that you’ll notice if you ever break the JSON encoding/decoding by renaming a property.

If you define init(from decoder: Decoder) or encode(to encoder: Encoder) to do custom decoding or encoding then it’ll be used for all of your encoding & decoding of that class. E.g., that email-only encoding for the User would be used if we created a new function to create new users, which is probably not what we want.

And That’s All for Parsing JSON in Swift 4 Today

Codable makes it simpler and cleaner to parse JSON in Swift 4. It’s flexible enough to handle discrepancies in keys and structure in the JSON relative to our code. So we don’t need to have ugly variable names like json_key_with_underscores in our Swift code.

For more info on this topic, Apple has some newly released docs:

Sample Code (Playground): Using JSON with Custom Types

Encoding and Decoding Custom Types

That’s all for today but in future posts we’ll be looking at Codable again, including handling dates and encoding/decoding of JSON that isn’t all key-value pairs.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.

Want more Swift tutorials like this one?

Sign up to get the latest GrokSwift tutorials and information about GrokSwift books sent straight to your inbox

Other Posts You Might Like