Using Custom Properties in Swift Decodable Objects

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.