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 Int
s 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)
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With