Parse JSON with different keys to same object using Codable in Swift

Viewed 940

I receive the following 2 responses from different APIs

{
  "id": "jdu72bdj",
  "userInfo": {
    "name": "Sudhanshu",
    "age": 28,
    "country": "India"
  }
}

and

{
  "profileId": "jdu72bdj",
  "profileDetails": {
    "name": "Sudhanshu",
    "age": 28,
    "country": "India"
  }
}

This is in context with iOS development using Swift language. Basically the object structure is same but keys are different. I'm parsing these using Codable, but I cannot think of a way to parse using same struct. All I can think of is making 2 different structs like this -

public struct Container1: Codable {
  public let id: String
  public let userInfo: UserProfile?    
}

and

public struct Container2: Codable {
  public let profileId: String
  public let profileDetails: UserProfile?    
}

They both use common UserProfile struct.

public struct UserProfile: Codable {
  public let name: String?
  public let age: Int?
  public let country: String?
}

Is there a way to use one common container struct for both responses which parse response from 2 different keys. I do not want Container1 and Container2 since they both have same structure.

Any suggestions ?

2 Answers

One solution is to use a custom key decoding strategy using an implementation of CodingKey found in Apple's documentation. The idea is to map the keys of both of the json messages to the properties of the struct Container that will be used for both messages.

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keys in
    let key = keys.last!.stringValue
    switch key {
    case "id", "profileId":
        return AnyKey(stringValue: "id")!
    case "userInfo", "profileDetails":
        return AnyKey(stringValue: "details")!
    default:
        return keys.last!
    }        
})

where the custom implementation of CodingKey is

struct AnyKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
        print(stringValue)
        self.stringValue = stringValue
        intValue = nil
    }

    init?(intValue: Int) {
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}

and then decode both json messages the same way using the below struct

struct Container: Codable {
    let id: String
    let details: UserProfile
}

let result = try decoder.decode(Container.self, from: data)

You can use your own init from decoder

struct UniversalProfileContainer: Decodable {
    struct UserProfile: Decodable {
        var name: String
        var age: Int
        var country: String
    }
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case profileId = "profileId"
        case userInfo = "userInfo"
        case profileDetails = "profileDetails"
    }

    let id: String
    let profile: UserProfile

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            id = try container.decode(String.self, forKey: .id)
        } catch {
            id = try container.decode(String.self, forKey: .profileId)
        }
        do {
            profile = try container.decode(UserProfile.self, forKey: .userInfo)
        } catch {
            profile = try container.decode(UserProfile.self, forKey: .profileDetails)
        }
    }
}

let first = """
{
  "id": "jdu72bdj",
  "userInfo": {
    "name": "Sudhanshu",
    "age": 28,
    "country": "India"
  }
}
"""
let second = """
{
  "profileId": "jdu72bdj",
  "profileDetails": {
    "name": "Sudhanshu",
    "age": 28,
    "country": "India"
  }
}
"""
let response1 = try? JSONDecoder().decode(UniversalProfileContainer.self,
                                            from: first.data(using: .utf8)!)

let response2 = try? JSONDecoder().decode(UniversalProfileContainer.self,
                                            from: second.data(using: .utf8)!)
Related