Swift Codable with dynamic keys

I have JSON structure as:

"periods": {
    "2018-06-07": [
      {
        "firstName": "Test1",
        "lastName": "Test1"
      }
    ],
    "2018-06-06": [
      {
        "firstName": "Test1",
        "lastName": "Test1"
      }
    ]
}

I tried to parse it like this:

public struct Schedule: Codable {
    public let periods: Periods
}

public struct Periods: Codable {
    public let unknown: [Inner]

    public struct Inner: Codable {
        public let firstName: String
        public let lastName: String
    }

    private struct CustomCodingKeys: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomCodingKeys.self)
        self.unknown = try container.decode([Inner].self, forKey: CustomCodingKeys(stringValue: "2018-06-06")
    }
}

But I can get result for only one value (2018-06-06). I have multiple dates here that I want to parse. Is this possible?


Solution 1:

Assuming you left out the { and } that would surround this block and are required for this to be valid JSON, the following is the simplest solution to getting things parsed, you really don't need to deal with CodingKey at all since your names match the keys in the JSON, so the synthesized CodingKey will work just fine:

public struct Schedule: Codable {
    public let periods : [String:[Inner]]
}

public struct Inner: Codable {
    public let firstName: String
    public let lastName: String
}

let schedule = try? JSONDecoder().decode(Schedule.self, from: json)

print(schedule?.periods.keys)

print(schedule?.periods["2018-06-07"]?[0].lastName)

The key is that the outer JSON is a JSON Object (dictionary/map) with a single key periods The value of that key is another map of arrays. Just break it down like that and everything falls out pretty much automatically.

Solution 2:

Ok, so I figured it out like this:

public struct Schedule: Codable {
    public let periods: Periods
}

public struct Periods: Codable {
    public var innerArray: [String: [Inner]]
    
    public struct Inner: Codable {
        public let firstName: String
        public let lastName: String
    }
    
    private struct CustomCodingKeys: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CustomCodingKeys.self)
        
        self.innerArray = [String: [Inner]]()
        for key in container.allKeys {
            let value = try container.decode([Inner].self, forKey: CustomCodingKeys(stringValue: key.stringValue)!)
            self.innerArray[key.stringValue] = value
        }
    }
}

As result I got dictionary like this: ["2018-06-06": [Inner]] where key is this Date String, and value Inner.