Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Decodable: how to transform one of values during decoding?

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?

like image 692
surfrider Avatar asked Aug 01 '17 05:08

surfrider


2 Answers

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

like image 42
pkamb Avatar answered Sep 25 '22 02:09

pkamb


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)
like image 60
Code Different Avatar answered Sep 25 '22 02:09

Code Different