Parsing a JSON Array Response into SwiftUI View

Hello everyone,

I have a bit of an issue that I expected to be fairly simple to solve. I have scoured the internet but have run out of solutions to test out.

Task:

I am trying to setup a very simple Dictionary API call from my iOS application with this link: https://api.dictionaryapi.dev/api/v2/entries/en/hello

And show the result of it in a basic View (using Text{}) with SwiftUI.

Problem:

The code compiles, but my ContentView is blank.

Debugging so far:

I followed a tutorial on setting up JSON parsing with the "Codable" protocol and was able to successfully set it up using a link that was a Movie API database. Upon further investigation on why the MovieAPI results were working and the DictionaryAPI results weren't, I noticed the the MovieAPI response was a JSONObject and the DictionaryAPI response was a JSONArray which starts with [ ]

After more googling I was sent to the solution to utilize something like this:

try! JSONDecoder().decode([DictionaryWord].self, from: jsonData)

But it also did not work and my view is still blank. I will be putting the code below as well for further reference. Thank you


import SwiftUI



struct ContentView: View {
    
    @State private var results = [DictionaryWord]()
    
    var body: some View {
        VStack{
            Text("API Example iOS Application")
            List(results, id: \.word) { item in
                VStack(alignment: .leading) {
                    Text(item.word)
                        .font(.headline)
                    Text(item.phonetic)
                }
            }
            .task {
                await loadData()
            }
        }
    }

    let freeDictionaryURL = "https://api.dictionaryapi.dev/api/v2/entries/en/hello"
    
    func loadData() async {
        guard let url = URL(string: freeDictionaryURL) else {
            print("Invalid URL")
            return
        }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            
            if let decoded: [WelcomeElement] = try? JSONDecoder().decode([DictionaryWord].self, from: data) {
                results = decoded
            }
            
        } catch {
            print("Invalid data")
        }
        
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

// MARK: - WelcomeElement
struct DictionaryWord: Codable {
    var word, phonetic: String
    var phonetics: [Phonetic]
    var origin: String
    var meanings: [Meaning]
}

// MARK: - Meaning
struct Meaning: Codable {
    var partOfSpeech: String
    var definitions: [Definition]
}

// MARK: - Definition
struct Definition: Codable {
    var definition, example: String
    var synonyms, antonyms: [String?]
}

// MARK: - Phonetic
struct Phonetic: Codable {
    var text: String
    var audio: String?
}




Accepted Reply

Try this code, works for me.

struct ContentView: View {
    
    @State private var results = [DictionaryWord]()
    
    var body: some View {
        VStack {
            Text("API Example iOS Application")
            List(results) { item in
                VStack(alignment: .leading) {
                    Text(item.word).font(.headline)
                    ForEach(item.phonetics) { phone in    // <--- here
                        if phone.text != nil {
                            Text(phone.text!)
                        }
                    }
                }
            }
        }
        .task {
            await loadData()
        }
    }
    
    let freeDictionaryURL = "https://api.dictionaryapi.dev/api/v2/entries/en/hello"
    
    func loadData() async {
        guard let url = URL(string: freeDictionaryURL) else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            // --- here
            let decoded: [DictionaryWord] = try JSONDecoder().decode([DictionaryWord].self, from: data)
            results = decoded
        } catch {
            print(error)  // <--- important
        }
    }
    
}

struct DictionaryWord: Identifiable, Codable {    // <--- here
    let id = UUID()
    
    let word: String
    let phonetics: [Phonetic]
    let meanings: [Meaning]
    let license: License
    let sourceUrls: [String]
    
    enum CodingKeys: String, CodingKey {
        case word, phonetics, meanings, license, sourceUrls
    }
}

struct License: Codable {
    let name: String
    let url: String
}

struct Meaning: Codable {
    let partOfSpeech: String
    let definitions: [Definition]
    let synonyms, antonyms: [String]?
}

struct Definition: Codable {
    let definition: String
    let synonyms, antonyms: [String]?
    let example: String?
}

struct Phonetic: Identifiable, Codable {    // <--- here
    let id = UUID()
    
    let audio: String
    let sourceURL: String?
    let license: License?
    let text: String?

    enum CodingKeys: String, CodingKey {
        case audio
        case sourceURL = "sourceUrl"
        case license, text
    }
}
  • Hey there, Thanks so much for responding with this solution. It worked like a charm without having to use any external libraries to parse. I really appreciate this + the comments with the differences. Cheers! 👍

  • Hey there, as I am working on this project I've come across an issue when trying to list another value such as: item.meanings I followed the same solution you recommended with your comment and updated the struct Meaning with the CodingKeys and UUID() but it will not work in a ForEach. Any ideas? Thank you again!

Add a Comment

Replies

JSONDecoder requires that the data being decoded represents a dictionary, not an array.

  • If you have any control over what data the server returns, the simplest solution is to change the server.

  • Or, if you trust the server to give you a top level JSON array reliably, you could simply manipulate the data to embed the server response in a single-key dictionary, then decode it using JSONDecoder.

  • Or, if you need more flexibility, you can always fall back to JSONSerialization, which is really an Obj-C API, to get an untyped object which you can then dig into to unravel the actual structure of JSON sub-objects. In that case, of course, you'd have to build your Swift structs "manually", without relying on Codable to synthesize decoding code for you,

  • Hey there, I really appreciate your response. Thanks for the suggestions, I will look into all of them and see which fits my situation best. I am going to give JSONDecoder a try and see if I can set it up. Cheers! 👍

Add a Comment

The first thing you should do is check for any errors and, if so, what they are.

Looking at your code, you aren't actually catching any potential errors if they are thrown. You are combining two different approaches: do-catch and if-let. Use one. I suggest changing your code to this:

do {
    let (data, _) = try await URLSession.shared.data(from: url)
            
    let decoded = try JSONDecoder().decode([DictionaryWord].self, from: data) {
    results = decoded            
} catch {
    print(error.localizedDescription)
}

I have also changed the print statement here which will give your more information on what the error is about. For example: the data is corrupted, a key is not found, a value is not found, or even a type mismatch. You can check the error against the cases listed in DecodingError.



At this point, I decided to test your code out because it all looked ok and the JSON data at the URL was definitely there. With a more thorough set of catch statements, I found out what the issue was.

Firstly, just something to remember, when retrieving data from an API, you need to know what to expect back. In the case of JSON data, what are the keys, value types, and data structures represented.

Your data models in code are expecting a set of keys to be present in the JSON data. However, in the data your are receiving some of those keys don't exist. For the URL you have provided these keys aren't in the JSON: DictionaryWord.phonetic, DictionaryWord.origin, Definition.example, Phonetic.text. For other words in the dictionary, their JSON data could be different and be missing other keys, or have all the keys and more. This is why you need to learn about the data you are expecting and is why you aren't able to decode the data with your current approach.

As a solution, you can either remove the missing keys directly from the data models, or mark them as optional so that the decoder knows they might not be present (preferable).

  • Hello there Thank you for your response. The changes to my do-catch + error response I will definitely implement! In regards to the JSON response, I utilized the Free Dictionary APIs main example response in their homepage link and I inputted it into the JSON Quicktype tool I found recommended online when creating Swift structs which I generates all the potential JSON values and marks as optional. Thanks again, cheers. 👍

Add a Comment

Try this code, works for me.

struct ContentView: View {
    
    @State private var results = [DictionaryWord]()
    
    var body: some View {
        VStack {
            Text("API Example iOS Application")
            List(results) { item in
                VStack(alignment: .leading) {
                    Text(item.word).font(.headline)
                    ForEach(item.phonetics) { phone in    // <--- here
                        if phone.text != nil {
                            Text(phone.text!)
                        }
                    }
                }
            }
        }
        .task {
            await loadData()
        }
    }
    
    let freeDictionaryURL = "https://api.dictionaryapi.dev/api/v2/entries/en/hello"
    
    func loadData() async {
        guard let url = URL(string: freeDictionaryURL) else {
            print("Invalid URL")
            return
        }
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            // --- here
            let decoded: [DictionaryWord] = try JSONDecoder().decode([DictionaryWord].self, from: data)
            results = decoded
        } catch {
            print(error)  // <--- important
        }
    }
    
}

struct DictionaryWord: Identifiable, Codable {    // <--- here
    let id = UUID()
    
    let word: String
    let phonetics: [Phonetic]
    let meanings: [Meaning]
    let license: License
    let sourceUrls: [String]
    
    enum CodingKeys: String, CodingKey {
        case word, phonetics, meanings, license, sourceUrls
    }
}

struct License: Codable {
    let name: String
    let url: String
}

struct Meaning: Codable {
    let partOfSpeech: String
    let definitions: [Definition]
    let synonyms, antonyms: [String]?
}

struct Definition: Codable {
    let definition: String
    let synonyms, antonyms: [String]?
    let example: String?
}

struct Phonetic: Identifiable, Codable {    // <--- here
    let id = UUID()
    
    let audio: String
    let sourceURL: String?
    let license: License?
    let text: String?

    enum CodingKeys: String, CodingKey {
        case audio
        case sourceURL = "sourceUrl"
        case license, text
    }
}
  • Hey there, Thanks so much for responding with this solution. It worked like a charm without having to use any external libraries to parse. I really appreciate this + the comments with the differences. Cheers! 👍

  • Hey there, as I am working on this project I've come across an issue when trying to list another value such as: item.meanings I followed the same solution you recommended with your comment and updated the struct Meaning with the CodingKeys and UUID() but it will not work in a ForEach. Any ideas? Thank you again!

Add a Comment