Waseem Mehany
Waseem Mehany

Reputation: 13

Parsing JSON with Swift 4 Decodable

I have been getting the following error every time I try to parse JSON in my program. I can't seem to figure it out.

"Expected to decode String but found an array instead.", underlyingError: nil

Here is the code I have been struggling with:

struct Book: Decodable {
    let id: Int
    let title: String
    let chapters: Int
    var pages: [Page]?
}

struct Page: Decodable {
    let id: Int
    let text: [String]
}

struct Chapter: Decodable {
    var chapterNumber: Int
}

func fetchJSON() {
    let urlString = "https://api.myjson.com/bins/kzqh3"
    guard let url = URL(string: urlString) else { return }

    URLSession.shared.dataTask(with: url) { (data, _, err) in
        if let err = err {
            print("Failed to fetch data from", err)
            return
        }
        guard let data = data else { return }
        do {
            let decoder = JSONDecoder()
            let books = try decoder.decode([Book].self, from: data)
            books.forEach({print($0.title)})
        } catch let jsonErr {
            print("Failed to parse json:", jsonErr)
        }
    }.resume()
}

Upvotes: 0

Views: 929

Answers (2)

Mario Burga
Mario Burga

Reputation: 1157

This is working:

import UIKit

class ViewController: UIViewController {

 private var books = [Book]()


    struct Book: Decodable {
        let id: Int
        let title: String
        let chapters: Int
        var pages: [Page]
    }

    struct Page: Decodable {
        let id: Int
        let text: [[String:String]]
    }




    override func viewDidLoad() {
        super.viewDidLoad()

        fetchJSON()
    }




    func fetchJSON() {
        let urlString = "https://api.myjson.com/bins/kzqh3"
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            if error != nil {
                print(error!.localizedDescription)
                return
            }
            guard let data = data else { return }
            do {
                let decoder = JSONDecoder()
                self.books = try decoder.decode([Book].self, from: data)

                DispatchQueue.main.async {

                    for info in self.books {
                        print(info.title)
                        print(info.chapters)
                        print(info.pages[0].id)
                        print(info.pages[0].text)
                        print("-------------------")
                    }

                }

            } catch let jsonErr {
                print("something wrong after downloaded: \(jsonErr) ")
            }
            }.resume()
    }

}

// print
//Genesis
//50
//1
//[["1": "In the beg...]]
//-------------------
//Exodus
//40
//2
//[["1": "In the beginning God created...]]
//

If you need to print the value of each chapter in the book, I can use something like this:

import UIKit

class ViewController: UIViewController {

 private var books = [Book]()


    struct Book: Decodable {
        let id: Int
        let title: String
        let chapters: Int
        var pages: [Page]
    }

    struct Page: Decodable {
        let id: Int
        let text: [[String:String]]
    }




    override func viewDidLoad() {
        super.viewDidLoad()

        fetchJSON()
    }




    func fetchJSON() {
        let urlString = "https://api.myjson.com/bins/kzqh3"
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            if error != nil {
                print(error!.localizedDescription)
                return
            }
            guard let data = data else { return }
            do {
                let decoder = JSONDecoder()
                self.books = try decoder.decode([Book].self, from: data)

                DispatchQueue.main.async {

                    for info in self.books {
                        print(info.title)
                        print(info.chapters)
                        print(info.pages[0].id)
                        //print(info.pages[0].text)

                        for cc in info.pages[0].text {

                            for (key, value) in cc {
                                print("\(key) : \(value)")
                            }
                        }

                        print("-------------------")
                    }

                }

            } catch let jsonErr {
                print("something wrong after downloaded: \(jsonErr) ")
            }
            }.resume()
    }

}


//Genesis
//50
//1
//1 : In the beginning God ...
//2 : But the earth became waste...
//.
//.
//.
//31 : And God saw everything...
//-------------------
//Exodus
//40
//2
//1 : In the beginning God...
//2 : But the earth became...
//.
//.
//.
//31 : And God saw everything

Upvotes: 1

vadian
vadian

Reputation: 285039

Are you sure that's the real error message?

Actually the error is supposed to be

"Expected to decode String but found a dictionary instead."

The value for key text is not an array of strings, it's an array of dictionaries

struct Page: Decodable {
    let id: Int
    let text: [[String:String]]
}

The struct Chapter is not needed.


Alternatively write a custom initializer and decode the dictionaries containing the chapter number as key and the text as value into an array of Chapter

struct Book: Decodable {
   let id: Int
   let title: String
   let chapters: Int
   let pages: [Page]
}

struct Page: Decodable {
    let id: Int
    var chapters = [Chapter]()

    private enum CodingKeys : String, CodingKey { case id, chapters = "text" }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        var arrayContainer = try container.nestedUnkeyedContainer(forKey: .chapters)
        while !arrayContainer.isAtEnd {
            let chapterDict = try arrayContainer.decode([String:String].self)
            for (key, value) in chapterDict {
                chapters.append(Chapter(number: Int(key)!, text: value))
            }
        }
    }
}

struct Chapter: Decodable {
    let number : Int
    let text : String
}

Upvotes: 1

Related Questions