Search code examples
swiftcodablejsondecoder

decode a Codable struct from API call and a mockup file simultaneously


I am currently working on a project where I am calling a webservice that returns me a JSON that I parse using Codable, like so :

My Struct:

struct User: Codable {
    var name: String
    var age: Int
}

API response :

{ "name": "Romiro", "age": 27 }

Decoding code :

let decoded = try! JSONDecoder().decode(User.self, from: data)

We decided to extend the User infos by adding new fields like so :

struct User: Codable {
    var name: String
    var age: Int
    var detail: Detail
}

struct Detail: Codable {
    var id: Int 
    var dob: Date 
}

However the backend is not developed yet, so the API response is still

{ "name": "Romiro", "age": 27 }

Is there a proper way to mock up only the var detail: Detail part, by loading it from a detail-mock.json file in project resources that matches the structure of Detail, but in the mean time keeping the API call for the pre-existing User part ?

By doing this, I would be able to keep all the logic of calling the endpoint, and shunting the only part that is under development, still by calling

let decoded = try! JSONDecoder().decode(User.self, from: data)

Furthermore, is there a way to do so without altering the json response from the API? I do not want to manually append the detail part to je json response.

Note: Obviously, the User struct is an example, on my project this is a much more complex struct


Solution

  • You can implement custom decoding on User, like this:

    struct User: Codable {
        var name: String
        var age: Int
        var detail: Detail
    
        enum CodingKeys: CodingKey {
            case name, age, detail
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
            age = try container.decode(Int.self, forKey: .age)
            if let detail = try container.decodeIfPresent(Detail.self, forKey: .detail) {
                self.detail = detail
            } else {
                let data = try Data(contentsOf: Bundle.main.url(forResource: "mockupDetail", withExtension: "json")!)
                self.detail = try JSONDecoder().decode(Detail.self, from: data)
            }
        }
    }
    

    Notice the if statement in init. That is where I decide whether to read the detail from the actual json, or the mocked json.

    This way, you don't need to make detail optional, but you would need to manually decode the other properties.