Search code examples
iosjsonswiftalamofiredecodable

Getting a particular value from different JSON responses in swift?


I have a model called College

class College : Decodable {
    let name : String
    let id : String
    let iconUrl : String
}

And a few college related APIs, each with a slightly different response. 2 examples are

  1. GET api/v1/colleges response JSON for this API is

    { "success": String, "colleges": [College] }

  2. GET api/v1/college/{collegeID} response JSON for this API is

    { "success": String, "college": College }

Now, from both the responses I need to get only the college information, the "success" key is not useful to me. My question is, how to get the college information without creating separate response models for each API? Currently I have implemented separate classes for each API response

class GetCollegesResponse : Decodable {
    let success : String
    let colleges : [College]
}
 
class GetCollegeResponse : Decodable {
    let success : String
    let college : College
}

And I use them in respective API calls like so

Alamofire.request(api/v1/colleges ....).responseJSON { response in
    let resp = JSONDecoder().decode(GetCollegesResponse.self, response.data)
    //get colleges from resp.colleges
}
 
Alamofire.request(api/v1/college/\(id) ....).responseJSON { response in
    let resp = JSONDecoder().decode(GetCollegeResponse.self, response.data)
    // get college form resp.college
}

Is there a simpler way to get this done?


Solution

  • Probably the right approach is to model the response as a generic type, like something like this:

    struct APIResponse<T: Decodable> {
       let success: String
       let payload: T
    }
    

    from which you could extract the payload.

    The problem is that the key with the payload changes: it's college for a single result and colleges for multiple college results.

    If you truly don't care and just want the payload, we could effectively ignore it and decode any key (other than "success") as an expected type T:

    struct APIResponse<T: Decodable> {
       let success: String
       let payload: T
    
       // represents any string key
       struct ResponseKey: CodingKey {
          var stringValue: String
          var intValue: Int? = nil
    
          init(stringValue: String) { self.stringValue = stringValue }
          init?(intValue: Int) { return nil }
       }
    
       init(from decoder: Decoder) throws {
          let container = try decoder.container(keyedBy: ResponseKey.self)
          
          let sKey = container.allKeys.first(where: { $0.stringValue == "success" })
          let pKey = container.allKeys.first(where: { $0.stringValue != "success" })
    
          guard let success = sKey, let payload = pKey else {
             throw DecodingError.keyNotFound(
                ResponseKey(stringValue: "success|any"),
                DecodingError.Context(
                   codingPath: container.codingPath, 
                   debugDescription: "Expected success and any other key"))
          }
    
          self.success = try container.decode(String.self, forKey: success)
          self.payload = try container.decode(T.self, forKey: payload)
       }
    }
    

    Then you could decode based on the expected payload:

    let resp = try JSONDecoder().decode(APIResponse<[College]>.self, response.data)
    let colleges = resp.payload