-4

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()
        }
    }
}
amelia
  • 71
  • 2
  • 7
  • 2
    Show the actual json that causes the trouble. Turn `data` into a string, format it nicely, and add it to the info in the question. Remove all the unnecessary gunk and reduce the question to be solely about the decoding error. – matt Mar 20 '23 at 02:36

1 Answers1

0

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
}
  • This is great thank you! It turns out it was just the missing query thing before a search is typed or when you clear it. – amelia Mar 20 '23 at 12:07
  • Is it problematic to leave an error message when the query is blank? – amelia Mar 20 '23 at 12:13
  • It is really up to you what you do if an error message is received. Do nothing (ignore it), or popup an alert, or whatever ... In your case, I see no real problems to ignoring it. You could modify your code to **not** proceed with a `searchBooks` if the `searchText` is empty. However there could be other error messages if the query text is malformed. – workingdog support Ukraine Mar 20 '23 at 12:22
  • In `sink` you could use this: `if !term.isEmpty { self?.searchBooks(for: term) }` to avoid an empty `searchText` query error from the server. – workingdog support Ukraine Mar 20 '23 at 12:34