Peter Mihok
Peter Mihok

Reputation: 47

SwiftUI decoding JSON from API

I know there are already some articles regarding this issue, but I could not find anything related to my JSON.

This is how my JSON likes like:

{
  "message": {
    "affenpinscher": [],
    "african": [],
    "airedale": [],
    "akita": [],
    "appenzeller": [],
    "australian": [
      "shepherd"
    ],
    "basenji": []
  },
  "status: "succes"
}

So, if I understand it correctly it is dictionary because it starts with {, but what are the things inside the "message"?

This is my Dog.swift class where I am re-writing the JSON, but I am not sure if it is correct:

class Dog: Decodable, Identifiable {
    
    var message: Message?
    var status: String?
}

struct Message: Decodable {
    
    var affenpinscher: [String:[String]]?
    var african: [String]?
    var airedale: [String]?
    var akita: [String]?
    var appenzeller: [String]?
    var australian: [String]?
    var basenji: [String]?
}

As you can see in the first value I was trying to play with data types, but no success.

I am decoding and parsing JSON here:

class ContentModel: ObservableObject {
    
    @Published var dogs = Message()
    
    init() {
        getDogs()
    }
    
    func getDogs(){
        
        // Create URL
        let urlString = Constants.apiUrl
        let url = URL(string: urlString)
        
        if let url = url {
            // Create URL request
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
            request.httpMethod = "GET"
             
            // Get URLSession
            let session = URLSession.shared
            
            // Create Data Task
            let dataTask = session.dataTask(with: request) { (data, response, error) in
                // Check that there is not an error
                if error == nil {
                    
                    do {
                        // Parse JSON
                        let decoder = JSONDecoder()
                        let result = try decoder.decode(Dog.self, from: data!)
                        print(result)
                        
                        // Assign result to the dogs property
                        DispatchQueue.main.async {
                            self.dogs = result.message!
                        }
                    } catch {
                        print(error)
                    }
                }
            }
            
            // Start the Data Task
            dataTask.resume()
        }
    }
    
}

And here I would love to iterate through it eventually, which I also have no idea how to do it:

struct ContentView: View {
    
    @EnvironmentObject var model: ContentModel
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVStack {
                    if model.dogs != nil {
//                        ForEach(Array(model.dogs.keys), id: \.self) { d in
//                            Text(d)
//                        }
                    }
                }
                .navigationTitle("All Dogs")
            }
            
        }
    }
}

enter image description here

What can I try next to resolve this?

Upvotes: 1

Views: 1338

Answers (2)

fuzzyCap
fuzzyCap

Reputation: 421

Using the native Swift approach that @vadian answered is a much lighter weight solution, but if you work with JSON often I'd recommend using SwiftyJSON.

You can parse the URL data task response into a Swifty json object like so:

import SwiftyJSON

guard let data = data, let json = try? JSON(data: data) else {
    return
}

// Make sure the json fetch was successful 
if json["status"].stringValue != "success" {
   return
}

Then you can access the message object safely without the verbosity of using Decodable. Here the message is parsed into an array of dog structs:

struct Dog {
    let name : String
    let types : [String]
}

var dogs: [Dog] = []

/// Load the docs into an array
for (name, typesJson) in json["message"].dictionaryValue {
    dogs.append(Dog(name: name, types: typesJson.arrayValue.map { $0.stringValue }))
}

print("dogs", dogs)

Upvotes: 0

vadian
vadian

Reputation: 285039

First of all don't use classes for a JSON model and to conform to Identifiable you have to add an id property and CodingKeys if there is no key id in the JSON.

My suggestion is to map the unhandy [String: [String]] dictionary to an array of an extra struct

I renamed Dog as Response and named the extra struct Dog

struct Dog {
    let name : String
    let types : [String]
}

struct Response: Decodable, Identifiable {
    
    private enum CodingKeys: String, CodingKey { case message, status }
    let id = UUID()
    
    let dogs: [Dog]
    let status: String
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        status = try container.decode(String.self, forKey: .status)
        let message = try container.decode([String:[String]].self, forKey: .message)
        dogs = message.map(Dog.init).sorted{$0.name < $1.name}
    }
}

In the model declare

@Published var dogs = [Dog]()

and decode

let result = try decoder.decode(Response.self, from: data!)
print(result)

// Assign result to the dogs property
DispatchQueue.main.async {
    self.dogs = result.dogs
}

The dogs array can be displayed seamlessly in a List


PS: Actually appenzeller is supposed to be

"appenzeller": ["sennenhund"],

or correctly in English

 "appenzell": ["mountain dog"],

😉😉😉

Upvotes: 2

Related Questions