The Decodable protocol makes it extremely simple to parse JSON from the web. However, one of the things I ran into recently was needing to have a property in one of my Decodable model objects that was not in the JSON I would be fetching. Having that property in the object would cause an error.
There are a couple of ways to address this. The first thing you could do, is simply define your property and provide a default value. Then, in your CodingKeys
enum, don’t include that property. Here’s an example of that:
struct RestaurantCategory: Decodable {
let title: String
let parents: [String]
// Not present in JSON
var isSelected = false
enum CodingKeys: String, CodingKey {
case title
case parents = "parent_categories"
}
}
In the particular case I was trying to solve, this would not work, as the property I needed to add was a CLLocationCoordinate2D
– not something I wanted to provide a default value for. Additionally, Decodable won’t automatically decode into this data type like it will for types such as String
or Int
.
To solve this, I defined my own init(from coder: Decoder)
initializer. This initializer is normally automatically be created by Decodable behind the scenes, but we get more control by implementing it ourselves. The goal of this class, by the way, is to have a MapKit annotation that can be decoded directly from JSON.
class Restaurant: NSObject, MKAnnotation, Decodable {
let name: String
let id: String
let rating: Double
let coordinateObject: Coordinate
// Property not present in JSON
let coordinate: CLLocationCoordinate2D
enum CodingKeys: String, CodingKey {
case name
case id
case rating
case coordinateObject = "coordinates"
}
// Manually initialize from decoder
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name: String = try container.decode(String.self, forKey: .name)
let id: String = try container.decode(String.self, forKey: .id)
let rating: Double = try container.decode(Double.self, forKey: .rating)
let coordinateObject: Coordinate = try container.decode(Coordinate.self, forKey: .coordinateObject)
// Initialize object not present in JSON
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: coordinateObject.latitude, longitude: coordinateObject.longitude)
self.name = name
self.id = id
self.rating = rating
self.coordinateObject = coordinateObject
self.coordinate = coordinate
}
}
This solution was straightforward and allowed me to provide a value for the property based on other values that were pulled in from the JSON.
As a note, the reason this object is subclassing from NSObject
is MKAnnotation
objects must conform to NSObjectProtocol
. To accomplish that, we just subclass NSObject
.