Search code examples
swiftswift-structs

How do I make a struct with nested json?


I have a JSON response from my api that returns this:

[
    {
        "id": 1,
        "chapter": 5,
        "amount": 28,
        "texts": [
            {
                "lyric": "lorem ipsum",
                "number": 1
            },
            {
                "lyric": "lorem foo bar",
                "number": 2
            }
        ],
        "book": 1
    }
]

I tried

struct Chapter: Decodable, Identifiable {
    var id: Int
    var chapter: Int
    var amount: Int

    struct Lyrics: Codable {
         var lyricText: String
         var lyricNumber: Int
     }

    enum Codingkeys: String, CodingKey {
        case lyricText = "lyric"
        case lyricNumber = "number"
    }
}

But I get the following error upon making the call


dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 0." UserInfo={NSDebugDescription=Invalid value around character 0.})))

My API call looks like this:

...
    @Published var chapters = [Chapter]()
    func fetchBookDetails() {
        if let url = URL(string: url) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error == nil {
                    if let safeData = data {
                        do {
                            
                            let response = try JSONDecoder().decode([Chapter].self, from: safeData)
                            DispatchQueue.main.async {
                                self.chapters = response
                            }
                        } catch {
                            print(error)
                        }
                        
                    }
                }
            }
            task.resume()
        }
    }

The struct looks fine I guess, but the api call is complaining - any idea what it could be? Or is it the struct that is done incorrectly


Solution

  • texts is a sub structure (an array of properties), so you need to define a second container for it, for example

    struct Text: Codable {
        let lyric: String
        let number: Int
    }
    

    Then you can update Chapter to reference the sub structure something like...

    struct Chapter: Decodable {
        let id: Int
        let chapter: Int
        let amount: Int
        let book: Int
        
        let texts: [Text]
    }
    

    And finally, load it...

    let chapters = try JSONDecoder().decode([Chapter].self, from: jsonData)
    

    But what about the error message?

    dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 0." UserInfo={NSDebugDescription=Invalid value around character 0.})))
    

    Oh, right, but the error message is telling there is something wrong with what you've downloaded. I like to, in these cases, convert the data to String and print it if possible, that way, you know what is been returned to you.

    For example:

    let actualText = String(data: safeData, encoding: .utf8)
    

    The print this and see what you're actually getting


    The Playground test code

    import UIKit
    
    let jsonText = """
    [
        {
            "id": 1,
            "chapter": 5,
            "amount": 28,
            "texts": [
                {
                    "lyric": "lorem ipsum",
                    "number": 1
                },
                {
                    "lyric": "lorem foo bar",
                    "number": 2
                },
            ],
            "book": 1
        }
    ]
    """
    
    struct Text: Codable {
        let lyric: String
        let number: Int
    }
    
    struct Chapter: Decodable {
        let id: Int
        let chapter: Int
        let amount: Int
        let book: Int
        
        let texts: [Text]
    }
    
    let jsonData = jsonText.data(using: .utf8)!
    do {
        let chapters = try JSONDecoder().decode([Chapter].self, from: jsonData)
    } catch let error {
        error
    }