Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type conversion with Swift 4's Codable

I'm playing around with the new Codable protocol in Swift 4. I'm pulling JSON data from a web API via URLSession. Here's some sample data:

{
  "image_id": 1,
  "resolutions": ["1920x1200", "1920x1080"]
}

I'd like to decode this into structs like this:

struct Resolution: Codable {
  let x: Int
  let y: Int
}

struct Image: Codable {
  let image_id: Int
  let resolutions: Array<Resolution>
}

But I'm not sure how to convert the resolution strings in the raw data into separate Int properties in the Resolution struct. I've read the official documentation and one or two good tutorials, but these focus on cases where the data can be decoded directly, without any intermediate processing (whereas I need to split the string at the x, convert the results to Ints and assign them to Resolution.x and .y). This question also seems relevant, but the asker wanted to avoid manual decoding, whereas I'm open to that strategy (although I'm not sure how to go about it myself).

My decoding step would look like this:

let image = try JSONDecoder().decode(Image.self, from data)

Where data is supplied by URLSession.shared.dataTask(with: URL, completionHandler: Data?, URLResponse?, Error?) -> Void)

like image 774
ACB Avatar asked Jul 13 '17 12:07

ACB


1 Answers

For each Resolution, you want to decode a single string, and then parse that into two Int components. To decode a single value, you want to get a singleValueContainer() from the decoder in your implementation of init(from:), and then call .decode(String.self) on it.

You can then use components(separatedBy:) in order to get the components, and then Int's string initialiser to convert those to integers, throwing a DecodingError.dataCorruptedError if you run into an incorrectly formatted string.

Encoding is simpler, as you can just use string interpolation in order to encode a string into a single value container.

For example:

import Foundation

struct Resolution {
  let width: Int
  let height: Int
}

extension Resolution : Codable {
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()

    let resolutionString = try container.decode(String.self)
    let resolutionComponents = resolutionString.components(separatedBy: "x")

    guard resolutionComponents.count == 2,
      let width = Int(resolutionComponents[0]),
      let height = Int(resolutionComponents[1])
      else {
        throw DecodingError.dataCorruptedError(in: container, debugDescription:
          """
          Incorrectly formatted resolution string "\(resolutionString)". \
          It must be in the form <width>x<height>, where width and height are \
          representable as Ints
          """
        )
      }

    self.width = width
    self.height = height
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode("\(width)x\(height)")
  }
}

You can then use it like so:

struct Image : Codable {

    let imageID: Int
    let resolutions: [Resolution]

    private enum CodingKeys : String, CodingKey {
        case imageID = "image_id", resolutions
    }
}

let jsonData = """
{
  "image_id": 1,
  "resolutions": ["1920x1200", "1920x1080"]
}
""".data(using: .utf8)!

do {
    let image = try JSONDecoder().decode(Image.self, from: jsonData)
    print(image)
} catch {
    print(error)
}

// Image(imageID: 1, resolutions: [
//                                  Resolution(width: 1920, height: 1200),
//                                  Resolution(width: 1920, height: 1080)
//                                ]
// )

Note we've defined a custom nested CodingKeys type in Image so we can have a camelCase property name for imageID, but specify that the JSON object key is image_id.

like image 103
Hamish Avatar answered Oct 11 '22 19:10

Hamish