'"Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil; GitHub repo showing app; SwiftUI

I need to create an app that will be able to return all the repositories that a GitHub user owns.

I created an app that contains of 3 files: CONTENT VIEW

import SwiftUI

struct ContentView: View {
    
    @StateObject var netManager = NetworkingManager()
    
    
    var body: some View {
            List {
                ForEach(netManager.owner) { item in
                    Text(item.reposUrl)
                }
        }
    }
}

API KEYS

import Foundation

struct Root : Decodable, Identifiable {
    let id: Int
    let items : [Repository]
}

struct Repository: Decodable, Identifiable {
    let id: Int
    let name, fullName: String
    let owner : Owner
}

struct Owner : Decodable, Identifiable {
    let id: Int
    let reposUrl : String
}

DECODERS (since I know I should need another one later, unless I can abstract this one enough)

class NetworkingManager: ObservableObject{
    @Published var owner = [Owner]()
    
    init() {
            loadData()
        }

        func loadData() {
            guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
            URLSession.shared.dataTask(with: url) {(data, _, _) in
                guard let data = data else { return }
                do {
                    let response = try JSONDecoder().decode(Owner.self, from: data)
                } catch {
                    print("error: \(error)")
                }
                
            }.resume()
        }
        
    }

The code runs fine, but I don't get any results (the first screen is blank) and I would like to see a list of the chosen user repos there. Could you please help me decode the dictionary?

I also wonder if the problem doesn't lie with that I didn't use convertFromSnakeCase key Decoding Strategy either, but I don't know how to put it there when the JSONDecoder is wrapped in a constant.



Solution 1:[1]

for a minimalist working example code, try this:

struct Repository: Decodable, Identifiable {
    let id: Int
    let name, fullName: String
    let owner: Owner
    
    enum CodingKeys: String, CodingKey {
        case id, name, owner
        case fullName = "full_name"  // <-- here
    }
}

struct Owner : Decodable, Identifiable {
    let id: Int
    let reposUrl : String

    enum CodingKeys: String, CodingKey, CaseIterable {
        case id
        case reposUrl = "repos_url"  // <-- here
    }
}

class NetworkingManager: ObservableObject{
    @Published var owner = [Owner]()
    
    init() {
        loadData()
    }
    
    func loadData() {
        guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
        URLSession.shared.dataTask(with: url) {(data, _, _) in
            guard let data = data else { return }
            DispatchQueue.main.async { // <-- here
                do {
                    let repos = try JSONDecoder().decode([Repository].self, from: data)  // <-- here
                    repos.forEach{ self.owner.append($0.owner) }
                } catch {
                    print("error: \(error)")
                }
           }
        }.resume()
    }
}

struct ContentView: View {
    @StateObject var netManager = NetworkingManager()
    
    var body: some View {
        List {
            ForEach(netManager.owner) { item in
                Text(item.reposUrl)
            }
        }
    }
}

This should give you a list of "https://api.github.com/users/jacobtoye/repos" because that is what the data consist of.

EDIT-1: to list all repos

class NetworkingManager: ObservableObject{
    @Published var repos = [Repository]() // <-- here repos
    
    init() {
        loadData()
    }
    
    func loadData() {
        guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
        URLSession.shared.dataTask(with: url) {(data, _, _) in
            guard let data = data else { return }
            DispatchQueue.main.async { // <-- here
                do {
                    self.repos = try JSONDecoder().decode([Repository].self, from: data)  // <-- here
                } catch {
                    print("error: \(error)")
                }
            }
        }.resume()
    }
}

struct ContentView: View {
    @StateObject var netManager = NetworkingManager()
    
    var body: some View {
        List {
            ForEach(netManager.repos) { repo in
              VStack {
                Text(repo.fullName).foregroundColor(.blue)
                Text(repo.owner.reposUrl)
              }
            }
        }
    }
}

Solution 2:[2]

One small amendment, after my recent project, in the solution suggested above it is better to use [weak self] if no more parameters are passed in the closure:

 guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
    URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
        guard let self = self, let data = data else { return }

instead of

guard let url = URL(string: "https://api.github.com/users/jacobtoye/repos") else { return }
        URLSession.shared.dataTask(with: url) {(data, _, _) in
            guard let data = data else { return }

Thanks to that approach, you can prevent memory leak, which might make your app slower at the end of the day.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Swantewit