1
votes

My JSON response is the following:

{
    "data": [
        {
            "unknown-key-c3e7f0": {
                "date_time": 1546944854000,
                "medication": "f4f25ea4-0607-4aac-b85a-edf40cc7d5b6",
                "record": {
                    "status": "never"
                }
            },
            "unknown-key-619d40": {
                "date_time": 1546944854000,
                "medication": "deef2278-f176-418f-ac34-c65fa54e712c",
                "record": {
                    "status": "always"
                }
            },
            "event": "06b445b9-dae0-48a1-85e4-b9f48c9a2349",
            "created": 1546949155020,
            "user": "8fb3fcd1-ffe6-4fd9-89b8-d22b1653cb6a",
            "id": "1546944855000",
            "type": "compliance"
        },
        {
            "unknown-key-619d40": {
                "date_time": 1546944975000,
                "medication": "deef2278-f176-418f-ac34-c65fa54e712c",
                "record": {
                    "status": "sometimes"
                }
            },
            "event": "7309d8e9-b71c-4068-b278-0ae6d91a57a6",
            "created": 1546946798407,
            "user": "8fb3fcd1-ffe6-4fd9-89b8-d22b1653cb6a",
            "id": "1546944975000",
            "type": "compliance"
        }
}

From the above response, I want to get the unknown keys and their values. The values of the unknown keys are of a custom type called Record which conforms to the Codable protocol.

I have created this struct for the parsing the data

struct RecordSuper: Codable
{
    var data: [[String: Record]]
}

So, I want to filter all other keys like event, created, user, etc which I am getting in the response and save only the unknown keys and the values. Please suggest how to parse this using codable.

I have gone through this answer as well as the variation suggested in the third comment of the answer. https://stackoverflow.com/a/46369152/8330469

This answer shows how to filter the incorrect data in an Array so that the correct data is not lost. I am trying to do something similar.

For example, I want to discard the event key because it is of type String and not of type Record.

The above answer will discard the whole dictionary because all the dictionaries are have incorrect data like event. And in the end, I get an empty array.

Thanks in advance.

1
Unless I misunderstood the code, that example you posted just filters items that failed to serialize by wrapping them in optionals. It doesn't look like it stores them. So they are lost. Not sure that works for you. These questions (which I honestly don't have an answer to but I did look myself) is the reason why I don't use Codable for decoding/encoding JSON data. I use my own framework MapCodableKit or if you prefer something more popular you can use ObjectMapper.Jacob
If you don't find an answer to your problem, I'd be happy to provide an example using one of the two frameworks above. MapCodableKit definately would work for you in this case and I'm pretty sure ObjectMapper would too. But I'm myself curious how someone solves this using Codable.Jacob
Yes, you are right. Sometimes it gets difficult in Codable. However, still, I would prefer Codable over libraries. Or even over manual deserialization. Codable is the future and I am hoping that Apple will further simplify it in the future. And these tough cases give us the opportunities to learn it better.Nilanshu Jaiswal
@Kubee Regardless of the parsing tool you have to be able to read and understand the structure of the JSON. Codable is very powerful and much more customizable than it seems to be.vadian

1 Answers

1
votes

This is a solution widely based on this intriguing answer of Rob Napier.

The goal of TitleKey and the two Decoder extensions is to map dictionaries with arbitrary keys to arrays adding the key as title property.

struct TitleKey: CodingKey {
    let stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

extension Decoder {
    func currentTitle() throws -> String {
        guard let titleKey = codingPath.last as? TitleKey else {
            throw DecodingError.dataCorrupted(.init(codingPath: codingPath,
                                                    debugDescription: "Not in titled container"))
        }
        return titleKey.stringValue
    }
}

extension Decoder {
    func decodeTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
        let titles = try container(keyedBy: TitleKey.self)
        return titles.allKeys.compactMap { title in
            return try? titles.decode(Element.self, forKey: title)
        }
    }
}

I modified the decodeTitledElements function to decode only those dictionaries whose value represents the RecordSuper struct filtering the other keys.

Here are the structs.

struct Root : Decodable {
    let data : [Containers]
}

struct Containers: Decodable {
    let containers: [RecordSuper]
    init(from decoder: Decoder) throws {
        self.containers = try decoder.decodeTitledElements(RecordSuper.self)
    }
}

struct RecordSuper : Decodable {
    let title : String
    let dateTime : Date
    let medication : String
    let record : Record

    enum CodingKeys: String, CodingKey {
        case dateTime = "date_time", medication, record
    }

    init(from decoder: Decoder) throws {
        self.title = try decoder.currentTitle()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.dateTime = try container.decode(Date.self, forKey: .dateTime)
        self.medication = try container.decode(String.self, forKey: .medication)
        self.record = try container.decode(Record.self, forKey: .record)
    }
}

struct Record : Decodable {
    let status : String
}

Now decode the JSON assuming jsonData is the JSON as Data

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
let result = try decoder.decode(Root.self, from: jsonData
print(result.data)