Swift Failable Decodable

Problem

First things first. Lets understand the problem. To do this lets look at situation based on the real world issue that drove me to tackle this.

Recently while working on Pra I ran into a situation where it was fetching pull requests from GitHub and then failing to decode the JSON payload response containing the collection of pull requests, resulting in getting back zero pull requests. This is an unacceptable experience.

The decoding was implemented in the default fashion as follows:

public struct PullRequestSearchResponseNode: Decodable {
  public let author: PullRequestSearchResponseAccount?
  public let title: String
  public let body: String
  public let closedAt: Date
  public let permalink: URL
  ...
}

public struct PullRequestSearchResponseDataSearch: Decodable {
  public let nodes: [PullRequestSearchResponseNode]
  public let pageInfo: PullRequestSearchResponsePageInfo
}

After some investigation I came to understand that it was failing due to not being able to properly decode one specific pull request because of effectively a type miss-match. For simplicity sake lets say the miss-match is happening because the PullRequestSearchResponseNode.closedAt property has a value in some pull request payloads but is null in others. This is a problem because our struct that we are trying to decode into states that it is expecting closedAt to always have a value.

Optionals

The correct, easiest, and most direct answer in the case of closedAt is to adjust the PullRequestSearchResponseNode struct so that the closedAt property is an Optional.

public struct PullRequestSearchResponseNode: Decodable {
  public let author: PullRequestSearchResponseAccount?
  public let title: String
  public let body: String
  public let closedAt: Date?
  public let permalink: URL
  ...
}

The Catch

Using an Optional is great. It fixes the problem and Pra is able to successfully decode all the pull requests again. However, there is one major catch. Sadly as much as we feel we know the specifications for a JSON payload it is likely we will miss something or maybe the JSON payload will change over time. Given this will inevitably happen we can't have that breaking our applications.

Possible Paths

So we need to come up with some way of handling the fact that we will miss something in the specification or that the specification will change.

All Properties Optional

One approach, not that I recommend it, is to make every property on the decodable struct an Optional. This would address a chunk of possible issues in decoding but not all possible issues. Also, it has the massive downside of then forcing the rest of the app to deal with both Optional paths everywhere in the code. Even if it is a property that will always be present.

Optional Wrapper

Another approach is to create a generic struct around T that wraps an option property of type T and provides decoding implementation. This could look something like the following:

public struct Failable<T: Decodable>: Decodable {
  let wrappedValue:T?

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    self.wrappedValue = try? container.decode(T.self)
  }
}

We would use this as follows:

public struct PullRequestSearchResponseDataSearch: Decodable {
  public let nodes: [Failable<PullRequestSearchResponseNode>]
  public let pageInfo: PullRequestSearchResponsePageInfo
}

This is nice as it effectively makes it so that we can make decoding failable at any of the various object boundaries. In the scenarios where decoding fails for some reason our wrappedValue would simply be null. This is definitely a step in the right direction. However, it has one major drawback. We have no idea why the decoding failed.

Result based Failable Decodable

Enter the Result type. This is exactly it's purpose, to manage optionality while also maintaining an associated error. So, instead of building a wrapper struct we can simply extend Result to implement the Decodable protocol and implement the constructor to handle appropriately assigning the success state in successful decodes, and assigning the failure state when decoding fails. This looks as follows:

extension Result: Decodable where Success: Decodable, Failure == DecodingError {
  public init(from decoder: Decoder) throws {
    do {
      let container = try decoder.singleValueContainer()
      self = .success(try container.decode(Success.self))
    } catch let err as Failure {
      self = .failure(err)
    }
  }
}

This is used as follows:

public struct PullRequestSearchResponseDataSearch: Decodable {
  public let nodes: [Result<PullRequestSearchResponseNode, DecodingError>]
  public let pageInfo: PullRequestSearchResponsePageInfo
}

This is great as we get the same ability to make objects failable decodables at any of the object boundaries but we also maintain all of the decoding errors to let us know why decoding failed for a given node.

Conclusion

If you are dealing with this situation you probably want to use the Result based Failable Decodable approach. I used this technique to solve the problems described in the Pra v1.4.0 Announcement. It facilitated collecting the decode failures and building the debug report so users can send them back to us.