How to parse Wikipedia JSON Dictionary with Swift? - ios

I'm new to the Wikipedia API and I have been trying to parse the image url from the API. The JSON I'm trying to parse is as follows:
API:
https://en.wikipedia.org/w/api.php?action=query&titles=tokyo&prop=pageimages&format=json
JSON Result:
{"batchcomplete": "",
"query": {
"normalized": [
{
"from": "tokyo",
"to": "Tokyo"
}
],
"pages": {
"30057": {
"pageid": 30057,
"ns": 0,
"title": "Tokyo",
"thumbnail": {
"source": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Skyscrapers_of_Shinjuku_2009_January.jpg/50px-Skyscrapers_of_Shinjuku_2009_January.jpg",
"width": 50,
"height": 27
},
"pageimage": "Skyscrapers_of_Shinjuku_2009_January.jpg"
}
}
}
}
Below are the struct I created to parse the data. I can see the url when I print it to the console, but since "source" is nested under "thumbnail", which lives in the value of the [String:Page] dict pair, I can't figure out a way to access it. How do I parse data in a dictionary like this? Thank you for your help in advance.
struct WikiAPIResults: Codable {
let batchcomplete: String?
let query: Query?
}
struct Query: Codable {
let normalized: [Normalized]?
let pages: [String:Pages]? // <- I can get to here
}
struct Normalized: Codable {
let from, to: String?
}
struct Pages: Codable {
let pageid, ns: Int?
let title: String?
let thumbnail: Thumbnail?
let pageimage: String?
}
struct Thumbnail: Codable {
let source: String? // <- But I want to grab this
let width, height: Int?
}
func fetchImageFromWikipedia(imageKeyword: String, completion: #escaping (WikiAPIResults) -> Void) {
var urlComponents = URLComponents(string: "https://en.wikipedia.org/w/api.php?")!
urlComponents.queryItems = [
"action": "query",
"titles": imageKeyword,
"prop": "pageimages",
"format": "json"].map { URLQueryItem(name: $0.key, value: $0.value) }
let task = URLSession.shared.dataTask(with: urlComponents.url!) { data, response, error in
let jsonDecoder = JSONDecoder()
if let data = data,
let result = try? jsonDecoder.decode(WikiAPIResults.self, from: data) {
completion(result)
}
}
task.resume()
}
fetchImageFromWikipedia(imageKeyword: "Tokyo") { result in
print(result.query?.pages?.values)
}

Do you mean gathering all thumbnail sources?
fetchImageFromWikipedia(imageKeyword: "Tokyo") { result in
let pages = result.query?.pages?.compactMap { $0.value } ?? []
let thumbnailSources = pages.compactMap { $0.thumbnail?.source }
print(thumbnailSources)
}

This works with your example perfectly:
import Foundation
struct Response: Decodable {
struct Query: Decodable {
struct NormalizedQuery: Decodable {
let from: String
let to: String
}
struct Page: Decodable {
struct Thumbnail: Decodable {
let source: URL
let width: Int
let height: Int
}
let id: Int
let ns: Int
let title: String
let thumbnail: Thumbnail
let pageImage: String
private enum CodingKeys: String, CodingKey {
case id = "pageid"
case ns
case title
case thumbnail
case pageImage = "pageimage"
}
}
let normalized: [NormalizedQuery]
let pages: [String : Page]
}
let batchComplete: String
let query: Query
private enum CodingKeys: String, CodingKey {
case batchComplete = "batchcomplete"
case query
}
}
let responseString = """
{
"batchcomplete" : "",
"query" : {
"normalized" : [
{
"from" : "tokyo",
"to" : "Tokyo"
}
],
"pages" : {
"30057" : {
"pageid" : 30057,
"ns" : 0,
"title" : "Tokyo",
"thumbnail" : {
"source" : "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Skyscrapers_of_Shinjuku_2009_January.jpg/50px-Skyscrapers_of_Shinjuku_2009_January.jpg",
"width" : 50,
"height" : 27
},
"pageimage" : "Skyscrapers_of_Shinjuku_2009_January.jpg"
}
}
}
}
"""
let responseData = responseString.data(using: .utf8)!
let response = try! JSONDecoder().decode(Response.self, from: responseData)
print(response)
// Response(batchComplete: "", query: __lldb_expr_18.Response.Query(normalized: [__lldb_expr_18.Response.Query.NormalizedQuery(from: "tokyo", to: "Tokyo")], pages: ["30057": __lldb_expr_18.Response.Query.Page(id: 30057, ns: 0, title: "Tokyo", thumbnail: __lldb_expr_18.Response.Query.Page.Thumbnail(source: https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Skyscrapers_of_Shinjuku_2009_January.jpg/50px-Skyscrapers_of_Shinjuku_2009_January.jpg, width: 50, height: 27), pageImage: "Skyscrapers_of_Shinjuku_2009_January.jpg")]))

Related

Elegant solutions to decoding a JSON nested array in swift using decodable

Currently working with an API, and while I have successfully gotten it to decode the full result, I am only interested in the Entities/identifier portion. While I have gotten it working and get what I want/need I feel like this could be done better and more elegant and maybe in a single step. Any insight/suggestions appreciated.
JSON returned from API:
{
"count": 4596,
"entities": [
{
"facet_ids": [
"contact",
"siftery",
"investor",
"ipqwery",
"aberdeen",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"bombora",
"key_event"
],
"identifier": {
"uuid": "468bef9f-2f50-590e-6e78-62e3adb05aa1",
"value": "Citi",
"image_id": "v1417152861/pdgwqt8ddecult5ktvdf.jpg",
"permalink": "citigroup",
"entity_def_id": "organization"
},
"short_description": "Citigroup is a diversified financial services holding company that provides various financial products and services."
},
{
"facet_ids": [
"contact",
"siftery",
"investor",
"apptopia",
"semrush",
"company",
"rank",
"builtwith",
"key_event"
],
"identifier": {
"uuid": "031a344b-c2b9-e60b-d950-1ae062026fde",
"value": "Citi",
"image_id": "yzlzhjqpparamrswaqa1",
"permalink": "citi-2",
"entity_def_id": "organization"
},
"short_description": "CITi is an NPO supporting the ICT sector in Western Cape."
},
{
"facet_ids": [
"contact",
"siftery",
"semrush",
"company",
"rank",
"builtwith",
"bombora"
],
"identifier": {
"uuid": "7ce45379-957c-49c5-bca2-c9ffd521f7da",
"value": "CITI",
"image_id": "qbkqndm7d0wgbogxjcrs",
"permalink": "citi-f7da",
"entity_def_id": "organization"
},
"short_description": "CITI trusted gateway to project-based change expertise that major organisations need to thrive, change and innovate."
}
]
}
Structs:
struct Entity: Decodable, Identifiable
{
var id: String
var companyName: String
var permalink: String
var imageID: String
init(from entity: Entities.Entity) throws
{
self.id = entity.identifier?.uuid ?? ""
self.companyName = entity.identifier?.value ?? ""
self.permalink = entity.identifier?.permalink ?? ""
self.imageID = entity.identifier?.image_id ?? ""
}
}
struct Entities: Decodable
{
var count:Int?
var entities: [Entity]?
struct Entity: Decodable
{
var facet_ids:[String]?
var identifier:Identifier?
var short_description:String?
}
struct Identifier:Decodable
{
var permalink:String?
var value:String?
var image_id:String?
var entity_def_id:String?
var uuid:String?
}
}
Call to decode:
if let data = data{
do {
let businessEntities = try decoder.decode(Entities.self, from: data)
let entities:[Entity] = try businessEntities.entities!.compactMap{
entity in
do
{
return try Entity(from: entity)
}
}
Thinking you are just interested in the Entities/identifier, you can simplify your model. Here's an example:
typealias Entities = [Entitie]
struct Entitie: Codable {
let facetIDS: [String]
let identifier: Identifier
let shortDescription: String
enum CodingKeys: String, CodingKey {
case facetIDS = "facet_ids"
case identifier
case shortDescription = "short_description"
}
}
struct Identifier: Codable {
let uuid, value, imageID, permalink: String
let entityDefID: String
enum CodingKeys: String, CodingKey {
case uuid, value
case imageID = "image_id"
case permalink
case entityDefID = "entity_def_id"
}
}
You can access entities object and decode it like this:
guard let data = data,
let jsonDict = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let entitiesObj = jsonDict["entities"] else { return }
do {
let entitiesData = try JSONSerialization.data(withJSONObject: entitiesObj)
let result = try JSONDecoder().decode(Entities.self, from: entitiesData)
} catch {
print(error.localizedDescription)
}
Obs: I created the CodingKeys because I use camelCase in my projects, you can left it in snake_case just remember to replace variables.

How to decode .keys from dictionary using Coadable

I have a below JSON response. And I want to fetch the allkeys of “data” which is [“rules”, “preference”, “goals”] using .keys method. But I couldn’t get the array of allkeys using .keys feature. I have attached my code snippet also. if you had faced this one, please suggest me to rid of this concern.
Although, I can get these allKeys using ObjectMapper and native Dictionary objects. I just need to know why I couldn't achieve this using Codable.
My json response
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
My code Snippet:
struct MeetingsDataModal: Codable {
let statusCode: Int?
let status: String?
let message: String?
let data: Results?
enum CodingKeys: String, CodingKey {
case statusCode = "statusCode"
case status = "status"
case message = "message"
case data = "data"
}
func allkeys() {
}
}
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
}
struct Rules : Codable {
}
struct Preference : Codable {
}
struct Goals : Codable {
}
My expectation
let content = try JSONDecoder().decode(MeetingsDataModal.self, from: (response as? Data)!)
print(content.data.keys)
But I am getting,
Value of type 'Results?' has no member 'keys'
Perhaps I am not understanding the question well but your "keys" are defined by your Codable protocol - so they are known. If you are using Swift 4.2+ you can take advantage of the CaseIterable protocol
struct Results: Codable {
let testOne: Int
let testTwo: String
enum CodingKeys: String, CodingKey, CaseIterable {
case testOne
case testTwo
}
}
Results.CodingKeys.allCases.map { $0.rawValue }
If you do need .keys. could add two-line code :
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
var keys : [String]{
return["rules", "preference","goals"]
}
}
Actually, if you don't like encoding/decoding way , we have another traditional JSON object can help you to handle unstructured JSON.
let obj = try JSONSerialization.jsonObject(with: response!, options: JSONSerialization.ReadingOptions.allowFragments)
print((((obj as! [String: Any?])["data"]) as! [String: Any?]).keys)
Here is one way to decode you data structure.
let d = """
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
""".data(using: .utf8)!
struct MeetingsDataModal: Decodable {
let statusCode: Int
let status: String
let message: String?
let data: Results
}
struct Results : Decodable {
let rules: Rules
let preference: [Int]
let goals: Goals
}
struct Rules : Decodable {
let goals : DirectData
}
struct DirectData : Decodable{
let min : String
let max : String
}
struct Goals : Decodable {
let total : Int
let data : [String]
}
let data0 = try JSONDecoder().decode(MeetingsDataModal.self, from: d)
print(data0)

How to extract and make specific arrays from JSON in Swift 4?

I am very new in swift and trying to extract information from a Json response and save them in some separate arrays.
Here is my Json:
{
"result": {
"date": [
{
"TestDate": 970606
},
{
"TestDate": 980703
}
],
"headId": [
{
"AreaCode": 200,
"AreaName": "center"
},
{
"AreaCode": 300,
"AreaName": "Middle"
}
]
}
}
I want to have an array for AreaName and another array for TestDate.
Can you please tell me how to do this?
You decode the JSON with Decodable and then pick off the pieces you want from it:
import Foundation
struct Response: Decodable {
struct Result: Codable {
let headID: [HeadID]
let date: [DateElement]
enum CodingKeys: String, CodingKey {
case headID = "headId"
case date
}
}
struct DateElement: Codable {
let testDate: Int
enum CodingKeys: String, CodingKey {
case testDate = "TestDate"
}
}
struct HeadID: Codable {
let areaCode: Int
let areaName: String
enum CodingKeys: String, CodingKey {
case areaCode = "AreaCode"
case areaName = "AreaName"
}
}
let result: Result
}
let data = """
{
"result": {
"headId": [
{
"AreaCode": 200,
"AreaName": "center"
},
{
"AreaCode": 300,
"AreaName": "Middle"
}
],
"date": [
{
"TestDate": 970606
},
{
"TestDate": 980703
}
]
}
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(Response.self, from: data)
let areaNames = response.result.headID.map { $0.areaName }
let testDates = response.result.date.map { $0.testDate }
print(areaNames)
print(testDates)
What I use that saves me a lot of time is a website that will translate the JSON structure to Swift for me. https://app.quicktype.io/
It's super duper helpful, and usually works perfectly just by copying and pasting the structures and classes into my Swift file.

Having trouble decoding JSON in Swift 4

Here is the response JSON:
{
"feed": {
"title": "Top Albums",
"id": "https://rss.itunes.apple.com/api/v1/us/apple-music/top-albums/all/25/explicit.json",
"author": {
"name": "iTunes Store",
"uri": "http://wwww.apple.com/us/itunes/"
},
"links": [
{
"self": "https://rss.itunes.apple.com/api/v1/us/apple-music/top-albums/all/25/explicit.json"
},
{
"alternate": "https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?genreId=34&popId=82&app=music"
}
],
"copyright": "Copyright © 2018 Apple Inc. All rights reserved.",
"country": "us",
"icon": "http://itunes.apple.com/favicon.ico",
"updated": "2018-07-17T01:41:38.000-07:00",
"results": [
{
"artistName": "Drake",
"id": "1405365674",
"releaseDate": "2018-06-29",
"name": "Scorpion",
"kind": "album",
"copyright": "℗ 2018 Young Money/Cash Money Records",
"artistId": "271256",
"artistUrl": "https://itunes.apple.com/us/artist/drake/271256?app=music",
"artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Music125/v4/5d/9b/97/5d9b97d6-9f78-e43b-7ba7-c2c42f53a166/00602567879121.rgb.jpg/200x200bb.png",
"genres": [
{
"genreId": "18",
"name": "Hip-Hop/Rap",
"url": "https://itunes.apple.com/us/genre/id18"
},
{
"genreId": "34",
"name": "Music",
"url": "https://itunes.apple.com/us/genre/id34"
}
],
"url": "https://itunes.apple.com/us/album/scorpion/1405365674?app=music"
},
...
And this is my code attempting to decode the above JSON:
struct object: Decodable {
struct feed: Decodable {
let title: String?
let id: Int?
struct Author: Decodable {
let name: String?
let uri: String?
}
let author: Author?
struct Link: Decodable {
let url: String?
private enum CodingKeys: String, CodingKey {
case url = "self"
}
}
let links: [Link]?
let copyright: String?
let country: String?
let icon: String?
let updated: String?
let results: [Album]?
}
}
struct Album: Decodable {
let artistName: String?
let id: Int?
let releaseDate: String?
let name: String?
let artworkUrl100: String?
let kind: String?
let copyright: String?
let artistId: Int?
let artistUrl: String?
struct Genre: Decodable {
let genreId: Int?
let name: String?
let url: String?
}
let genres: [Genre]?
let url: String?
}
I am attempting to obtain the results with album name, artist name and image url. I am having difficulty understanding how to construct a suitable structure to obtain the data with a nested json.
When I attempt to obtain the data within my completion block, i just end up getting nil values for everything.
//Networking
let jsonString = "https://rss.itunes.apple.com/api/v1/us/apple-music/top-albums/all/25/explicit.json"
guard let url = URL(string: jsonString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let obj = try JSONDecoder().decode(object.feed.self, from: data)
print(obj)
} catch let jsonError {
print("Error with json", jsonError)
}
}.resume()
And my result is all nil:
feed(title: nil, id: nil, author: nil, links: nil, copyright: nil, country: nil, icon: nil, updated: nil, results: nil)
You have a few issues.
Firstly, the outermost structure is an object under the key feed
You need:
struct object: Decodable {
let feed: Feed
}
struct Feed: Decodable {
let title: String
let id: String
let author: Author
struct Link: Decodable {
let url: String?
private enum CodingKeys: String, CodingKey {
case url = "self"
}
}
let links: [Link]
let copyright: String
let country: String
let icon: String
let updated: String
let results: [Album]
}
and then you would decode it via:
do {
let obj = try JSONDecoder().decode(object.self, from: data)
print(obj)
} catch let jsonError {
print("Error with json", jsonError)
}
Note that all of the "ids" are Strings not Ints

Modelling an 'object' type to decode JSON

I'm making multiple calls to the Apple Music API which is returning an array of Resource objects. The Resource object has a property called attributes of type object which varies depending on whether it is a Song, Playlist, or Album.
How do I model the resource object so I can decode the JSON and create Song, Playlist, and Album objects?
struct Resource {
let id : String?
let type : String?
let href : String?
let attributes : "What goes here?"
}
Update 1:
I tried Joe's second recommendation and set up my structs like so:
struct Resource: Decodable{
enum Attribute {
case song(Song), playlist(Playlist)
}
let id : String?
let type : String?
let href : String?
let attributes: Attribute?
enum CodingKeys : CodingKey{
case id, type, href, attributes
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
type = try values.decode(String.self, forKey: .type)
href = try values.decode(String.self, forKey: .href)
switch type {
case "library-songs":
attributes = try .song(values.decode(Song.self, forKey: .attributes))
case "library-playlists":
attributes = try .playlist(values.decode(Playlist.self, forKey: .attributes))
default:
attributes = nil
}
}
}
struct Song : Decodable {
let playParams : PlayParams?
let trackNumber : Int?
let durationInMillis : Int?
let name : String?
let albumName : String?
let artwork : Artwork?
let contentRating : String?
let artistName : String?
}
struct Artwork : Decodable {
let width : Int?
let height : Int?
let url : String?
}
struct PlayParams : Decodable {
let id : String?
let kind : String?
let isLibrary : Bool?
}
The sample JSON looks like this for song:
{
"data": [
{
"id": "i.4YBNbl3IXVJQRM",
"type": "library-songs",
"href": "/v1/me/library/songs/i.4YBNbl3IXVJQRM",
"attributes": {
"albumName": "\"Awaken, My Love!\"",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music71/v4/00/d0/d7/00d0d743-b0de-31d8-09eb-0796269bb555/UMG_cvrart_00044003187658_01_RGB72_1800x1800_16UMGIM77118.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 326933,
"playParams": {
"id": "i.4YBNbl3IXVJQRM",
"kind": "song",
"isLibrary": true
},
"artistName": "Childish Gambino",
"trackNumber": 6,
"name": "Redbone",
"contentRating": "explicit"
}
},
{
"id": "i.mmpeOrZiLqoKOv",
"type": "library-songs",
"href": "/v1/me/library/songs/i.mmpeOrZiLqoKOv",
"attributes": {
"albumName": "Funk Wav Bounces Vol. 1",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is5-ssl.mzstatic.com/image/thumb/Music127/v4/8b/37/23/8b372308-f764-d03a-5bda-a7a456292547/886446469607.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 272659,
"playParams": {
"id": "i.mmpeOrZiLqoKOv",
"kind": "song",
"isLibrary": true
},
"artistName": "Calvin Harris",
"trackNumber": 4,
"name": "Rollin (feat. Future & Khalid)",
"contentRating": "explicit"
}
},
{
"id": "i.JL1aVxNtzmYDJG",
"type": "library-songs",
"href": "/v1/me/library/songs/i.JL1aVxNtzmYDJG",
"attributes": {
"albumName": "The Weekend (Funk Wav Remix) - Single",
"artwork": {
"width": 1200,
"height": 1200,
"url": "https://is4-ssl.mzstatic.com/image/thumb/Music118/v4/aa/d5/e5/aad5e5e3-dff5-7d8f-5747-b695ad9f2299/886446852157.jpg/{w}x{h}bb.jpg"
},
"durationInMillis": 171806,
"playParams": {
"id": "i.JL1aVxNtzmYDJG",
"kind": "song",
"isLibrary": true
},
"artistName": "SZA & Calvin Harris",
"trackNumber": 1,
"name": "The Weekend (Funk Wav Remix)"
}
}
]
}
When I decode the data using:
let resource = try? JSONDecoder().decode([Resource].self, from: data!)
print(resource!)
I get a fatal error: Unexpectedly found nil while unwrapping an Optional value
Update 2:
I figured out the issue. The extra layer on top was screwing up the decoding. I added another level:
struct Welcome: Codable {
let data: [Datum]
}
struct Datum: Codable {
let id, type, href: String
let attributes: Attributes
}
struct Attributes: Codable {
let albumName: String
let artwork: Artwork
let durationInMillis: Int
let playParams: PlayParams
let artistName: String
let trackNumber: Int
let name: String
let contentRating: String?
}
struct Artwork: Codable {
let width, height: Int
let url: String
}
struct PlayParams: Codable {
let id, kind: String
let isLibrary: Bool
}
This just works for songs but I will try reimplementing for both songs and playlists.

Resources