2

I have a JSON that has ID's in the root level:

{
    "12345": {
        "name": "Pim"
    },
    "54321": {
        "name": "Dorien"
    }
}

My goal is to use Codable to create an array of User objects that have both name and ID properties.

struct User: Codable {
    let id: String
    let name: String
}

I know how to use Codable with a single root level key and I know how to work with unknown keys. But what I'm trying to do here is a combination of both and I have no idea what to do next.

Here's what I got so far: (You can paste this in a Playground)

import UIKit

var json = """
{
    "12345": {
        "name": "Pim"
    },
    "54321": {
        "name": "Dorien"
    }
}
"""

let data = Data(json.utf8)

struct User: Codable {
    let name: String
}

let decoder = JSONDecoder()
do {
    let decoded = try decoder.decode([String: User].self, from: data)
    decoded.forEach { print($0.key, $0.value) }
    // 54321 User(name: "Dorien")
    // 12345 User(name: "Pim")
} catch {
    print("Failed to decode JSON")
}

This is what I'd like to do:

let decoder = JSONDecoder()
do {
    let decoded = try decoder.decode([User].self, from: data)
    decoded.forEach { print($0) }
    // User(id: "54321", name: "Dorien")
    // User(id: "12345", name: "Pim")
} catch {
    print("Failed to decode JSON")
}

Any help is greatly appreciated.

Pim
  • 2,128
  • 18
  • 30

1 Answers1

4

You can use a custom coding key and setup User as below to parse unknown keys,

struct CustomCodingKey: CodingKey {

    let intValue: Int?
    let stringValue: String

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

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

struct UserInfo: Codable {
    let name: String
}

struct User: Codable {
    var id: String = ""
    var info: UserInfo?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomCodingKey.self)
        if let key = container.allKeys.first {
            self.id = key.stringValue
            self.info = try container.decode(UserInfo.self, forKey: key)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CustomCodingKey.self)
        if let key = CustomCodingKey(stringValue: self.id) {
            try container.encode(self.info, forKey: key)
        }
    }
}

let decoder = JSONDecoder()
do {
    let decoded = try decoder.decode(User.self, from: data)
    print(decoded.id) // 12345
    print(decoded.info!.name) // Pim
} catch {
    print("Failed to decode JSON")
}
Kamran
  • 14,987
  • 4
  • 33
  • 51
  • 1
    Don't forget to set up the custom encoder though. Or use Decodable instead of Codable. – Casper Zandbergen Oct 07 '19 at 09:51
  • This is great. Only thing left to do getting all properties in the same object, like so: User { let id: String, let name: String }. I'm sure I'll figure that one out. – Pim Oct 07 '19 at 16:58
  • I just realized this way I'm getting a single user object, while I was expecting an array of users. I'll update my question to make this more clear. – Pim Oct 07 '19 at 17:02
  • Your response structure is getting worse. `Id`, `name` or other `user` related properties should have been in the same object returned as an array. If that was not possible then the current structure should be sent as an array otherwise there is no option then decoding it as `Dictionary` (i.e, `[String: UserInfo].self`) and then `map` each dictionary to `User`. – Kamran Oct 07 '19 at 17:32
  • 1
    I couldn't agree with you more. Unfortunately this is the situation I have to deal with... Thank you for taking the time to come up with your answer. – Pim Oct 07 '19 at 17:42