jsonswiftgoogle-booksgoogle-books-api

Why am I getting a CodingKeys error on the actual object I'm searching?


When I change a letter in the searchText, I receive this error.

decoding error keyNotFound(CodingKeys(stringValue: "items", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "items", intValue: nil) ("items").", underlyingError: nil))

I'm at a loss because “items” are what I'm actually searching so making it optional doesn't seem right. (like I've seen as the solution for others) It feels like there's a bigger problem that I am missing.

Here is the json if you search harry potter - https://www.googleapis.com/books/v1/volumes?q=harry+potter

{
  "kind": "books#volumes",
  "totalItems": 1588,
  "items": [
    {
      "kind": "books#volume",
      "id": "L18VBQAAQBAJ",
      "etag": "vz78Ah8lJGE",
      "selfLink": "https://www.googleapis.com/books/v1/volumes/L18VBQAAQBAJ",
      "volumeInfo": {
        "title": "The Psychology of Harry Potter",
        "subtitle": "An Unauthorized Examination Of The Boy Who Lived",
        "authors": [
          "Neil Mulholland"
        ],
        "publisher": "BenBella Books",
        "publishedDate": "2007-04-10",
        "description": "Harry Potter has provided a portal to the wizarding world for millions of readers, but an examination of Harry, his friends and his enemies will take us on yet another journey: through the psyche of the Muggle (and wizard!) mind. The twists and turns of the series, as well as the psychological depth and complexity of J. K. Rowling’s characters, have kept fans enthralled with and puzzling over the many mysteries that permeate Hogwarts and beyond: • Do the Harry Potter books encourage disobedience? • Why is everyone so fascinated by Professor Lupin? • What exactly will Harry and his friends do when they finally pass those N.E.W.T.s? • Do even wizards live by the ticking of the clock? • Is Harry destined to end up alone? And why did it take Ron and Hermione so long to get together? Now, in The Psychology of Harry Potter, leading psychologists delve into the ultimate Chamber of Secrets, analyzing human mind and motivation by examining the themes and characters that make the Harry Potter books the bestselling fantasy series of all time. Grab a spot on the nearest couch, and settle in for some fresh revelations about our favorite young wizard!",
        "industryIdentifiers": [
          {
            "type": "ISBN_13",
            "identifier": "9781932100884"
          },
          {
            "type": "ISBN_10",
            "identifier": "1932100881"
          }
        ],
        "readingModes": {
          "text": false,
          "image": false
        },
        "pageCount": 338,
        "printType": "BOOK",
        "categories": [
          "Literary Criticism"
        ],
        "averageRating": 3.5,
        "ratingsCount": 5,
        "maturityRating": "NOT_MATURE",
        "allowAnonLogging": false,
        "contentVersion": "0.1.2.0.preview.0",
        "panelizationSummary": {
          "containsEpubBubbles": false,
          "containsImageBubbles": false
        },
        "imageLinks": {
          "smallThumbnail": "http://books.google.com/books/content?id=L18VBQAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
          "thumbnail": "http://books.google.com/books/content?id=L18VBQAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
        },
        "language": "en",
        "previewLink": "http://books.google.com/books?id=L18VBQAAQBAJ&printsec=frontcover&dq=harry+potter&hl=&cd=1&source=gbs_api",
        "infoLink": "http://books.google.com/books?id=L18VBQAAQBAJ&dq=harry+potter&hl=&source=gbs_api",
        "canonicalVolumeLink": "https://books.google.com/books/about/The_Psychology_of_Harry_Potter.html?hl=&id=L18VBQAAQBAJ"
      },
      "saleInfo": {
        "country": "US",
        "saleability": "NOT_FOR_SALE",
        "isEbook": false
      },
      "accessInfo": {
        "country": "US",
        "viewability": "PARTIAL",
        "embeddable": true,
        "publicDomain": false,
        "textToSpeechPermission": "ALLOWED",
        "epub": {
          "isAvailable": false
        },
        "pdf": {
          "isAvailable": false
        },
        "webReaderLink": "http://play.google.com/books/reader?id=L18VBQAAQBAJ&hl=&source=gbs_api",
        "accessViewStatus": "SAMPLE",
        "quoteSharingAllowed": false
      },
      "searchInfo": {
        "textSnippet": "Now, in The Psychology of Harry Potter, leading psychologists delve into the ultimate Chamber of Secrets, analyzing human mind and motivation by examining the themes and characters that make the Harry Potter books the bestselling fantasy ..."
      }
    }
}

Book Model

import Foundation

struct ApiResponse: Codable {
    let items: [Book]
}

struct Book: Codable, Identifiable {
    let id: String?
    let volumeInfo: VolumeInfo
}

struct VolumeInfo: Codable {
    let title: String
    let authors: [String]?
    let categories: [String]?
    let description: String?
    let industryIdentifier: [IndustryIdentifier]?
    let imageLinks: ImageLinks
}

struct ImageLinks: Codable {
    let thumbnail: URL?
}

struct IndustryIdentifier: Codable {
    let identifier: String
}

SearchBookViewModel

import Foundation
import Combine

class SearchBookViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var books = [Book]()
    
    let limit: Int = 20
    
    var subscriptions = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { [weak self] term in
                self?.searchBooks(for: term)
            }.store(in: &subscriptions)
    }
    
    func searchBooks(for searchText: String) {
        if let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(searchText)") {

            URLSession.shared.dataTask(with: url) { data, response, error in
                
                if let data = data {
                    
                    do {
                        let response = try JSONDecoder().decode(ApiResponse.self, from: data)
                        DispatchQueue.main.async {
                            self.books = response.items
                        }
                    } catch {
                        print("decoding error \(error)")
                    }
                }
            }.resume()
        }
    }
}

Solution

  • You need to make sure the struct models you have match the json data exactly. To do that, you need to read the docs of the server responses, to determine which fields are optionals, for example, in VolumeInfo you should have let imageLinks: ImageLinks?.

    Also you need to cater for when the server returns an error message, for example when the searchText is empty, as it is at the begining, leading to decoding of it into your ApiResponse.

    Try this simple example code, and build on that for your own purpose. Note, there many other approaches, this is just an example to avoid your decoding error.

    import Foundation
    import SwiftUI
    import Combine
    
    struct ContentView: View {
        @StateObject var viewModel = SearchBookViewModel()
    
        var body: some View {
            VStack {
                TextField("search for", text: $viewModel.searchText).border(.red)
                List(viewModel.books) { book in
                    Text(book.volumeInfo.title)
                }
            }
        }
    }
    
    class SearchBookViewModel: ObservableObject {
        @Published var searchText = ""
        @Published var books = [Book]()
        
        let limit: Int = 20
        var subscriptions = Set<AnyCancellable>()
        
        init() {
             $searchText
                 .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
                 .sink { [weak self] term in
                     self?.searchBooks(for: term)
                 }.store(in: &subscriptions)
         }
        
        func searchBooks(for searchText: String) {
            if let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(searchText)") {
                URLSession.shared.dataTask(with: url) { data, response, error in
                    if let data = data {
                        // to show what you get from the server
                        //  print("---> data: \(String(data: data, encoding: .utf8) as AnyObject)")
                        do {
                            let response = try JSONDecoder().decode(ApiResponse.self, from: data)
                            DispatchQueue.main.async {
                                if let books = response.items {  // <-- here
                                    self.books = books
                                } else {
                                    self.books = []
                                    // todo, deal with the error
                                    print("---> error \(response.error)")
                                }
                            }
                        } catch {
                            // todo, deal with decoding errors
                            print("decoding error \(error)")
                        }
                    }
                }.resume()
            }
        }
    }
    
    struct ErrorMesage: Codable {  // <-- here
        let domain: String
        let message: String
        let reason: String
        let location: String
        let locationType: String
    }
    
    struct ApiError: Codable {  // <-- here
        let code: Int
        let message: String
        let errors: [ErrorMesage]
    }
    
    struct ApiResponse: Codable {
        let items: [Book]?   // <-- here
        let error: ApiError? // <-- here
    }
    
    struct Book: Codable, Identifiable {
        let id: String?
        let volumeInfo: VolumeInfo
    }
    
    struct VolumeInfo: Codable {
        let title: String
        let authors: [String]?
        let categories: [String]?
        let description: String?
        let industryIdentifier: [IndustryIdentifier]?
        let imageLinks: ImageLinks?  // <-- here
    }
    
    struct ImageLinks: Codable {
        let thumbnail: URL?
    }
    
    struct IndustryIdentifier: Codable {
        let identifier: String
    }