0

I have struct that conforms to protocol Codable. Some properties are decoded from JSON fields so I use the CodingKeys enum for that. But there are few properties that are not in my JSON and I need to calculate them from decoded JSON properties. For example, if you get a Zip code from JSON, I want to calculate City from it.

I don't want City to be an optional String. So I try to calculate it right after my Zip code field is decoded from JSON.

struct Place: Codable {
   var name: String
   var zipcode: String
   // ... Lot of other properties decoded from JSON

   var city: String // This property has to be calulated after `zip code` is decoded

   enum CodingKeys: String, CodingKey {
      case name = "placeName"
      case zipcode = "NPA"
      // other properties from JSON
   }
}

I've tried this solution to rewrite init(from decoder: Decoder). But that means I need to manually write each property I need to decode. As I have a lot, I would prefer to let default init decoder does it job, and then add my code to calculate City.

Is there a way to do something like : call default init with decoder, then add some code ?

I was also thinking about computed property. But as calculating City from Zip code is quite lot of code, I don't want that it is always computed.

I need something like :

init(from decoder: Decoder) throws {
   // <- Call default init from decoder
   city = CityHelper.city(from: zipcode) // quite heavy code in there
}
Jonathan
  • 606
  • 5
  • 19

4 Answers4

1

I would prefer to let default init decoder does it job, and then add my code to calculate City

Unfortunately you can't. It is currently all or nothing; you cannot treat the synthesized init as some sort of inheritance from super (as in your imagined Call default init).

I was also thinking about computed property. But as calculating City from Zip code is quite lot of code, I don't want that it is always computed.

Use a lazy var property whose initializer calls a method that transforms zip to city. That way it is calculated, but just once. The zip will not change, so this is an acceptable compromise.

Or even better, use a reducer to transform the decoded struct (with zip) into a completely different struct (with city).

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • It looks like a good compromise in that case, yes! But that means first time this property is accessed, it could freeze a bit while it is calculated. And of course, later it won't be anymore. That's why I wanted to calculate everything when data is decoded from JSON / loaded. – Jonathan Dec 29 '21 at 21:41
  • That's exactly why I said a reducer was better. Download, decode, and reduce. All done. – matt Dec 29 '21 at 21:42
  • Yep, I think my comment was right before your edit ^^ Could you provide me a link about that "reducer" solution or an example? I don't figure out how it works. – Jonathan Dec 29 '21 at 21:47
  • This just means you have a way of turning one struct into another struct. You can call that a reducer or a transformer. – matt Dec 29 '21 at 21:59
0

I have found a solution but it needs to use class instead of struct.

I store decoded properties in a parent class. Then I extend this class and add calculated properties in it.

As it is not possible to extend struct, I had to use class. Maybe it is a first step to a better solution and it can help anyone find something better.

class DecodedPlace: Codable {
    var name: String
    var zipcode: String
    
    enum CodingKeys: String, CodingKey {
        case name = "placeName"
        case zipcode = "NPA"
    }
}

class Place: DecodedPlace {
    var city: String

    required init(from decoder: Decoder) throws {
        city = "" // Default value just to call super.init.
        
        try super.init(from: decoder)
        
        city = CityHelper.city(from: zipcode)
    }
}
Jonathan
  • 606
  • 5
  • 19
-1

Ok misread at first. Here's something I think could help: use willSet or didSet on zip to then compute the city perhaps? Not sure if that gets you around not having a default value but this code will only run if the zipcode changes

struct Place: Codable {
   var name: String
    var zipcode: String{
        didSet{
            //do something after zip is set like calculate city?
        }
        willSet{
            //do something before zip is set.
        }
    }
   // ... Lot of other properties decoded from JSON

   var city: String // This property has to be calulated after `zip code` is decoded

   enum CodingKeys: String, CodingKey {
      case name = "placeName"
      case zipcode = "NPA"
      // other properties from JSON
   }
}
jimBeaux27
  • 117
  • 7
  • Unfortunately it doesn't want to compile because 'Type 'Place' does not conform to protocol 'Decodable''. It is because my property var city:String does not have a default value. I've tried setting city property as optional, but it doesn't work. didSet is not called on init? – Jonathan Dec 29 '21 at 21:59
  • "didSet is not called on init" Correct. – matt Dec 29 '21 at 22:00
-2

You can use a computed property that relies on the zip to return a city. So you'd have to figure out the logic of how to map zip back to city with some method but here's a simple example I cooked up:

struct Rectangle : Codable{
        var width : Float
        var height : Float
        
        var area : Float{
            return width * height
        }
    }

Usage:

let myRect = Rectangle(width: 5.0, height: 5.0)
 print(myRect.area)

Result: 25.0

jimBeaux27
  • 117
  • 7
  • Look at the last paragraph of the question. A simple computed property won't do. – matt Dec 29 '21 at 21:37
  • Thank you. But with that solution your area property is calculated each time you call it. As my code for "city from zip code" is quite heavy, I don't want to use a computed property. – Jonathan Dec 29 '21 at 21:37
  • Ok fair point. See my other solution using didSet/willSet – jimBeaux27 Dec 29 '21 at 21:55