Home > front end >  SwiftUI Structuring and Decoding JSON
SwiftUI Structuring and Decoding JSON

Time:06-23

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)
  • Related