Using URLSession to get crystal data from my custom API. Trying to list by title.
I know something's incorrect here but not sure what. Looked all over SO for JSON dictionary decoding but no luck.
Is a Response
struct superfluous or can I simply decode [String:Crystal]
with JSONDecoder()
? Any and all direction and suggestion is appreciated. I knew how to do this with arrays but dictionaries are tripping me up.
GET https://lit-castle-74820.herokuapp.com/api/crystals
{
"amethyst": {
"composition": "silicon dioxide",
"formation": "forms when gas bubbles occur in lava and become trapped",
"colour": "purple",
"metaphysical": [
"calming",
"healing",
"especially for headaches",
"fatigue, & anxiety"
]
},
"clear quartz": {
"composition": "silicon dioxide",
"formation": "forms when gas bubbles occur in lava and become trapped",
"colour": "colourless or appears white",
"metaphysical": "master healer for all ailments; amplifies the healing vibration of other stones placed nearby"
},
"moss agate": {
"composition": "silicon dioxide, commonly featuring manganese or iron",
"formation": "formed from weathered volcanic rock",
"colour": "colourless with specks of white, green, blue, or brown",
"metaphysical": [
"gentle healing",
"promotes tranquility",
"cures physical ailments (inflammation, cold & flu)"
]
},
"carnelian": {
"composition": "silicon dioxide with iron impurity",
"formation": "formed from a combination of the silica minerals quartz and moganite",
"colour": "orange or red often featuring yellow",
"metaphysical": [
"promotes life-force",
"vitality",
"energizes body and mind"
]
},
"spirit quartz": {
"composition": "silicon dioxide",
"formation": "forms when gas bubbles occur in lava and become trapped",
"colour": "purple, yellowish brown, light grey",
"metaphysical": [
"assists in spiritual journey",
"uplifts and promotes vibration"
]
},
"amazonite": {
"composition": "potassium feldspar",
"formation": "formed in deep sea igneous rocks that cool very slowly",
"colour": "blue or green with white speckles or lines",
"metaphysical": [
"Soothes anxiety and overthinking",
"helps heal emotional trauma"
]
},
"tourmaline": {
"composition": "silicate of boron and aluminum",
"formation": "Pegmatite pockets underground that slowly cool and form crystals",
"colour": "black or pink",
"metaphysical": [
"repels negative energy",
"highly protective"
]
},
"pyrite": {
"composition": "iron sulfide",
"formation": "forms in sedimentary rocks in low oxygen environments",
"colour": "gold",
"metaphysical": [
"abundance",
"good luck",
"emotional strength"
]
}
}
struct Response: Codable {
let crystals: [String:Crystal]
}
struct Crystal: Codable, Identifiable {
var id = UUID()
let composition, formation, color: String
let metaphysical: [String]
}
struct ContentView: View {
@State private var crystals: [String:Crystal] = [:]
var body: some View {
List(crystals) { crystal in
(crystal.key)
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://lit-castle-74820.herokuapp.com/api/crystals") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data else { return }
do {
let decodedResponse = try JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
self.crystals = decodedResponse.crystals
}
} catch let jsonError as NSError {
print("JSON decode failed: \(jsonError)")
}
}.resume()
}
}
CodePudding user response:
This is a somewhat strange API. The problem you are facing is within the Crystal
struct. metaphysical
seems to be an array of String
but in at least one case it is a simple String
.
Edit:
As this is a custom API you should edit it and return an array of String
even if there is only one element in the collection.
In addition:
- Your decoding approach does not work as there is no top element in the JSON. It´s a
[String:Crystal]
. - You have a typo in your struct
color
->colour
Then you can use:
try JSONDecoder().decode([String:Crystal].self, from: data)
Original:
If this data is static (does not change) you could get away with the following solution:
struct Response: Codable {
let amethyst, mossAgate, carnelian, spiritQuartz, amazonite, tourmaline, pyrite: GemWithArray
let clearQuartz: GemWithoutArray
enum CodingKeys: String, CodingKey {
case amethyst
case clearQuartz = "clear quartz"
case mossAgate = "moss agate"
case carnelian
case spiritQuartz = "spirit quartz"
case amazonite, tourmaline, pyrite
}
}
struct GemWithArray: Codable {
let composition, formation, colour: String
let metaphysical: [String]
}
struct GemWithoutArray: Codable {
let composition, formation, colour, metaphysical: String
}
And decoding it like:
try JSONDecoder().decode(Response.self, from: data)
But there is a way to make this more robust and capable of treating it as [String:GemWithArray]
. You would need to use a custom initializer in the GemWithArray
struct. In it try to decode metaphysical
to an [String]
and if it fails create an array, decode and append it as String
.
struct GemWithArray: Codable {
let composition, formation, colour: String
let metaphysical: [String]
enum CodingKeys: String, CodingKey{
case composition, formation, colour, metaphysical
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
composition = try container.decode(String.self, forKey: .composition)
formation = try container.decode(String.self, forKey: .formation)
colour = try container.decode(String.self, forKey: .colour)
if let metaphysical = try? container.decode([String].self, forKey: .metaphysical){
self.metaphysical = metaphysical
} else{
metaphysical = [try container.decode(String.self, forKey: .metaphysical)]
}
}
}
and decoding it like:
try JSONDecoder().decode([String:GemWithArray].self, from: data)