简体   繁体   中英

Swift: Codable for complex JSON

I get JSON response when I hit wiki API as shown below. I find it complex to decode it.

{
    "continue": {
        "picontinue": 452160,
        "continue": "||pageterms"
    },
    "query": {
        "pages": [
            {
                "pageid": 164053,
                "ns": 0,
                "title": "Abdullah II of Jordan",
                "index": 2,
                "terms": {
                    "description": [
                        "King of the Hashemite Kingdom of Jordan"
                    ]
                }
            },
            {
                "pageid": 348097,
                "ns": 0,
                "title": "Abdullah Ahmad Badawi",
                "index": 9,
                "terms": {
                    "description": [
                        "Malaysian politician"
                    ]
                }
            },
            {
                "pageid": 385658,
                "ns": 0,
                "title": "Abdelaziz Bouteflika",
                "index": 8,
                "thumbnail": {
                    "source": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Abdelaziz_Bouteflika_casts_his_ballot_in_May_10th%27s_2012_legislative_election_%28cropped%29.jpg/37px-Abdelaziz_Bouteflika_casts_his_ballot_in_May_10th%27s_2012_legislative_election_%28cropped%29.jpg",
                    "width": 37,
                    "height": 50
                },
                "terms": {
                    "description": [
                        "President of Algeria"
                    ]
                }
            },
            {
                "pageid": 452160,
                "ns": 0,
                "title": "Abdul Qadeer Khan",
                "index": 7,
                "terms": {
                    "description": [
                        "Pakistani nuclear scientist"
                    ]
                }
            },
            {
                "pageid": 2028438,
                "ns": 0,
                "title": "Abdelbaset al-Megrahi",
                "index": 6,
                "terms": {
                    "description": [
                        "Libyan mass murderer"
                    ]
                }
            },
            {
                "pageid": 4709709,
                "ns": 0,
                "title": "Abdul",
                "index": 1,
                "terms": {
                    "description": [
                        "family name"
                    ]
                }
            },
            {
                "pageid": 18950786,
                "ns": 0,
                "title": "Abdul Hamid II",
                "index": 5,
                "thumbnail": {
                    "source": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/07/Abdul_Hamid_2.jpg/35px-Abdul_Hamid_2.jpg",
                    "width": 35,
                    "height": 50
                },
                "terms": {
                    "description": [
                        "34th sultan of the Ottoman Empire"
                    ]
                }
            },
            {
                "pageid": 19186951,
                "ns": 0,
                "title": "Abdullah of Saudi Arabia",
                "index": 4,
                "terms": {
                    "description": [
                        "former King of Saudi Arabia"
                    ]
                }
            },
            {
                "pageid": 25955055,
                "ns": 0,
                "title": "Abdullah of Pahang",
                "index": 10,
                "terms": {
                    "description": [
                        "Sultan of Pahang"
                    ]
                }
            },
            {
                "pageid": 36703624,
                "ns": 0,
                "title": "Abdel Fattah el-Sisi",
                "index": 3,
                "thumbnail": {
                    "source": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Abdel_Fattah_el-Sisi_in_2017.jpg/39px-Abdel_Fattah_el-Sisi_in_2017.jpg",
                    "width": 39,
                    "height": 50
                },
                "terms": {
                    "description": [
                        "Current President of Egypt"
                    ]
                }
            }
        ]
    }
}

I am interested only in array of WikiPage data from the above JSON where structure should look something like this.

struct Wikipage: Decodable {
    var pageid: Int?
    var thumbnail: String?
    var title: String?
    var description: String?
    enum CodingKeys: String, CodingKey {
        case query
        case pages
        case terms
        case description
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let query = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .query)
        print(query)
    }
}

I am not able to dig further into query here as I get a data mismatch error. Can I be able to decode without using extra classes/vars to hold unwanted data? How do we decode in this case?

        let wiki = try decoder.decode([Wikipage].self, from: data!)

So I was able to get query to decode a container. However, pages does not work because it contains and array not [String: Any]. Look at this question and you can see that it is possible to decode nested values in a more direct manner like you want. However, those values are directly accessible, where the values you are trying to decode are in an array of pages so this method won't work for your JSON.

For example what value should pageid , thumbnail , title , or description have? There are several pages in your JSON. I don't think you want one object with those variables for every page?

In any case I think your best solution here is to nest your decoding as suggested in this answer . The idea is to decode the full object first (RawResponse) then just take your desired values and save them in a smaller object (Wikipage). I assumed you wanted the values for the first page.

struct RawResponse: Codable {
    let query: Query

    enum CodingKeys: String, CodingKey {
        case query
    }
}

struct Query: Codable {
    let pages: [Page]
}

struct Page: Codable {
    let pageid: Int?
    let title: String?
    let terms: Terms?
    let thumbnail: Thumbnail?
}

struct Terms: Codable {
    let description: [String]
}

struct Thumbnail: Codable {
    let source: String
    let width, height: Int
}


struct Wikipage: Decodable {

    var pageid: Int?
    var thumbnail: String?
    var title: String?
    var description: String?

    init(from decoder: Decoder) throws {
        let rawResponse = try RawResponse(from: decoder)

        let pageZero = rawResponse.query.pages.first

        self.pageid = pageZero?.pageid
        self.thumbnail = pageZero?.thumbnail?.source
        self.title = pageZero?.title
        self.description = pageZero?.terms?.description.first
    }
}

The syntax you're asking for is not possible.

let wiki = try decoder.decode([Wikipage].self, from: data!)

This decodes an Array of Wikipage. There is already a decoder for that in stdlib which doesn't do what you want. You can't replace it. You need your own type that wraps it up. That type doesn't have to do anything except wrap up the result, but it has to exist. Here's how you build it.

First, a WikiPage should be just the Wikipage portion. It should not try to know anything about its container. So it would look something like this:

struct Wikipage {
    let pageid: Int
    let thumbnail: URL?
    let title: String
    let description: String
}

extension Wikipage: Decodable {
    private enum CodingKeys: String, CodingKey {
        case pageid
        case title
        case thumbnail
        case terms
    }

    private struct Thumbnail: Decodable { let source: String }

    private struct Terms: Decodable { let description: [String] }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.pageid = try container.decode(Int.self, forKey: .pageid)
        self.title = try container.decode(String.self, forKey: .title)

        let source = try container.decodeIfPresent(Thumbnail.self,
                                                   forKey: .thumbnail)?.source
        self.thumbnail = source.flatMap(URL.init(string:))

        self.description = try container.decode(Terms.self,
                                                forKey: .terms).description.first ?? ""
    }
}

I've removed the optionality of things that don't need to be optional, and elevated things like URLs to proper types. Notice the private helper structs (Thumbnail and Terms). These take care of nesting.

The same approach applies to the top-level. There's no need to decode anything you don't want, but you do need a type to hold it.

struct WikipageResult: Decodable {
    let pages: [Wikipage]

    private enum CodingKeys: String, CodingKey {
        case query
    }
    private struct Query: Decodable {
        let pages: [Wikipage]
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.pages = try container.decode(Query.self, forKey: .query).pages
    }
}

And finally to use it:

let pages = try JSONDecoder().decode(WikipageResult.self, from: json).pages

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM