8

Be default, Decodable protocol makes translation of JSON values to object values with no change. But sometimes you need to transform value during json decoding, for example, in JSON you get {id = "id10"} but in your class instance you need to put number 10 into property id (or into even property with different name).

You can implement method init(from:) where you can do what you want with any of the values, for example:

public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    latitude = try container.decode(Double.self, forKey:.latitude)
    longitude = try container.decode(Double.self, forKey: .longitude)
    // and for example there i can transform string "id10" to number 10
    // and put it into desired field
}

Thats sounds great for me, but what if i want to change value just for one of the JSON fields and left all my other 20 fields with no change? In case of init(from:) i should manually get and put values for every of 20 fields of my class! After years of objC coding it's intuitive for me to first call super's implementation of init(from:) and then make changes just to some fields, but how i can achieve such effect with Swift and Decodable protocol?

surfrider
  • 1,376
  • 14
  • 29
  • 1
    Compare [Swift 4 JSON Decodable simplest way to decode type change](https://stackoverflow.com/q/44594652/2976878) – one option would be a wrapper type. – Hamish Aug 01 '17 at 06:40
  • I looked to code snippet by your link. Seems i have to implement `init(from:)` method for all the properties anyway? – surfrider Aug 01 '17 at 07:16
  • No, the first two examples in my answer using `StringCodableMap` didn't implement `init(from:)`; they relied on the auto-generated `Codable` conformance. – Hamish Aug 01 '17 at 07:19

2 Answers2

5

You can use a lazy var. The downside being that you still have to provide a list of keys and you can't declare your model a constant:

struct MyModel: Decodable {
    lazy var id: Int = {
        return Int(_id.replacingOccurrences(of: "id", with: ""))!
    }()
    private var _id: String

    var latitude: CGFloat
    var longitude: CGFloat

    enum CodingKeys: String, CodingKey {
        case latitude, longitude
        case _id = "id"
    }
}

Example:

let json = """
{
    "id": "id10",
    "latitude": 1,
    "longitude": 1
}
""".data(using: .utf8)!

// Can't use a `let` here
var m = try JSONDecoder().decode(MyModel.self, from: json)
print(m.id)
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • Thanks! It could be a workaround in some cases if one can accept a violation of the model's immutability. – surfrider Jun 25 '21 at 07:56
2

Currently you are forced to fully implement the encode and decode methods if you want to change the parsing of even a single property.

Some future version of Swift Codable will likely allow case-by-case handling of each property's encoding and decoding. But that Swift feature work is non-trivial and hasn't been prioritized yet:

Regardless, the goal is to likely offer a strongly-typed solution that allows you to do this on a case-by-case basis with out falling off the "cliff" into having to implement all of encode(to: and init(from: for the benefit of one property; the solution is likely nontrivial and would require a lot of API discussion to figure out how to do well, hence why we haven't been able to do this yet.

- Itai Ferber, lead developer on Swift 4 Codable

https://bugs.swift.org/browse/SR-5249?focusedCommentId=32638

pkamb
  • 33,281
  • 23
  • 160
  • 191
  • 1
    Thanks. Interesting. "falling off the cliff" - excellent describing phrase :) Well, at least I glad to hear they considered the problem. – surfrider Jun 25 '21 at 07:54