Swift 4 added the Codable
protocol that automatically handles binding Data to custom model types. A typical example of this is binding JSON returned from a REST call to a model. Swift's JSONDecoder.decode()
implements all the functionality required, e.g.:
[
{
"id" : 1,
"author" : "Rin Tin Tin",
"quote" : "Woof"
}
]
struct QuoteModel: Codable {
let id: Int
let author: String
let quote: String
}
Additionally, JSONDecoder.decode()
allows us to just bind the fields we're interested in. For example, if we're not interested in the id
field we can bind to:
struct QuoteModel: Codable {
let author: String
let quote: String
}
Conversly, an exception is thrown if the model contains a field the data doesn't e.g. extraField
that is not in the JSON:
struct QuoteModel: Codable {
let author: String
let quote: String
let extraField: String
}
Additionally, the thrown error "The data couldn’t be read because it is missing.
" doesn't provide much information about what caused the exception.
Similarly, if a model field's type doesn't match the JSON's field type the error "The data couldn’t be read because it isn’t in the correct format.
" is thrown, without any information about which field has the incorrect type.
API you depend on can change without warning. This is particuarly true when the API is in development. An app that used to run may break even though no code changes were made. Such errors can be hard to track down, especially given that the localized error messages do not provide pertinent info.
Here are some defensive steps you can take to avoid these mysterious breakings and to help you know when the server's JSON structure has changed.
Make all model fields optional e.g.:
struct QuoteModel: Codable {
let id: Int?
let author: String?
let quote: String?
let extraField: String?
}
This will prevent JSONDecoder.decode()
from throwing an exception when it encounters missing fields in the JSON.
Use failable initializers for view model e.g.:
struct QuoteViewModel {
let attributedQuote: String
}
extension QuoteViewModel {
init?(quoteModel: QuoteModel) {
guard let author = quoteModel.author, let quote = quoteModel.quote else { return nil }
guard author.count > 0, quote.count > 0 else { return nil }
attributedQuote = "\(quote) –\(author)"
}
}
This way you can use compactMap
to filter out mal formed data e.g.:
let viewModel = models.compactMap { QuoteViewModel(quoteModel: $0) }
Viewing Data
in the debugger doesn't reveal information about the Data
's contents. Xcode's Data
Quick Look plugin helps visualize Data
content.
Click the Quick Look icon to see a visualization of the Data
's content:
When alternating between GET, POST, PUT and DELETE REST API it is often helpful to log the full URL with parameters and the HTTP method.
extension URLSession {
open func dataTaskDebug(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
print("\(request.httpMethod ?? "method unspecified") \(request.url?.absoluteString ?? "nil URL")")
return URLSession.shared.dataTask(with: request, completionHandler: completionHandler)
}
}
When showing an issue to the team responsible for the API layer, demonstrate the problem with a REST client like RESTED, it eliminates any discussion of where the issue actually lies.
- Add support for incorrect types
- Add support for logging missing data to console.