BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

SOLVED: Proyect 12: SwiftData Making a Class conform to Codable

Forums > 100 Days of SwiftUI

I am trying to make two classes conform to Codable.

It throws this error with the Book class:

"Cannot convert value of type 'volumeInfo' to expected argument type 'volumeInfo.Type'"

How could I solve it?

import Foundation
import SwiftData

@Model
class Book: Codable, Identifiable {

    enum codingKeys: String, CodingKey {
        case id
        case volumeInfo
    }

    var id: UUID
    var volumeInfo: volumeInfo

    init(id: UUID, volumeInfo: volumeInfo) {
        self.id = id
        self.volumeInfo = volumeInfo
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: codingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        volumeInfo = try container.decode(volumeInfo.self, forKey: .volumeInfo)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: codingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(volumeInfo, forKey: .volumeInfo)
    }
}

@Model
class volumeInfo: Codable {

    enum codingKeys: String, CodingKey {
    case title
    case authors
    case publisher
    case argument = "description"
    case pages = "pageCount"
    case releaseDate = "publishedDate"
    case googleBooksRating = "averageRating"
    }

    var title: String
    var authors: [String]?
    var publisher: String
    var argument: String
    var pages: Int
    var releaseDate: Date
    var googleBooksRating: Double

    init(title: String, authors: [String]? = nil, publisher: String, argument: String, pages: Int, releaseDate: Date, googleBooksRating: Double) {
        self.title = title
        self.authors = authors
        self.publisher = publisher
        self.argument = argument
        self.pages = pages
        self.releaseDate = releaseDate
        self.googleBooksRating = googleBooksRating
    }

    static let exampleVolumeInfo = volumeInfo(title: "Someone you know", authors: ["John Doe"], publisher: "Some Publisher", argument: "An example argument", pages: 345, releaseDate: .now, googleBooksRating: 4.5)

    required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: codingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        authors = try container.decodeIfPresent([String].self, forKey: .authors)
        publisher = try container.decode(String.self, forKey: .publisher)
        argument = try container.decode(String.self, forKey: .argument)
        pages = try container.decode(Int.self, forKey: .pages)
        releaseDate = try container.decode(Date.self, forKey: .releaseDate)
        googleBooksRating = try container.decode(Double.self, forKey: .googleBooksRating)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: codingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encodeIfPresent(authors, forKey: .authors)
        try container.encode(publisher, forKey: .publisher)
        try container.encode(argument, forKey: .argument)
        try container.encode(pages, forKey: .pages)
        try container.encode(releaseDate, forKey: .releaseDate)
        try container.encode(googleBooksRating, forKey: .googleBooksRating)
    }
}

   

Thanks for your great advices and for your help Obelix!!! I solved it... !!!!

I have continued with the code to get a list of the books from my json file (just their names to start) but it shows nothing...

Sure I am not doing several things right despite trying my best... it is really difficult for me because I have never done something related with this... but I am here and continue learning!!!

Could you please tell me what I am doing wrong to try to fix it ???

Sometimes I feel embarrassed here for asking my questions, because I think they might be really easy or silly... but it is the way I find to solve them and to improve...

import Foundation
import SwiftData

@Model
class Response: Codable {

    var results: [Book]

    enum codingKeys: String, CodingKey {
        case results
    }

    init(results: [Book]) {
        self.results = results
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: codingKeys.self)
        results = try container.decode([Book].self, forKey: .results)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: codingKeys.self)
        try container.encode(results, forKey: .results)
    }
}

@Model
class Book: Codable, Identifiable {

    enum codingKeys: String, CodingKey {
        case id
        case aBookInMyLibrary = "volumeInfo"
    }

    var id: UUID
    var aBookInMyLibrary: VolumeInfo

    static let BookExample = Book(id: UUID(), aBookInMyLibrary: VolumeInfo.exampleVolumeInfo)
    static let BookExample2 = Book(id: UUID(), aBookInMyLibrary: VolumeInfo.example2VolumeInfo)
    static let BooksExample: [Book] = [BookExample, BookExample2]

    init(id: UUID, aBookInMyLibrary: VolumeInfo) {
        self.id = id
        self.aBookInMyLibrary = aBookInMyLibrary
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: codingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        aBookInMyLibrary = try container.decode(VolumeInfo.self, forKey: .aBookInMyLibrary)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: codingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(aBookInMyLibrary, forKey: .aBookInMyLibrary)
    }
}

@Model
class VolumeInfo: Codable {

    enum codingKeys: String, CodingKey {
    case title
    case authors
    case publisher
    case argument = "description"
    case pages = "pageCount"
    case releaseDate = "publishedDate"
    case googleBooksRating = "averageRating"
    }

    var title: String
    var authors: [String]
    var publisher: String
    var argument: String
    var pages: Int
    var releaseDate: Date
    var googleBooksRating: Double

    init(title: String, authors: [String], publisher: String, argument: String, pages: Int, releaseDate: Date, googleBooksRating: Double) {
        self.title = title
        self.authors = authors
        self.publisher = publisher
        self.argument = argument
        self.pages = pages
        self.releaseDate = releaseDate
        self.googleBooksRating = googleBooksRating
    }

    static let exampleVolumeInfo = VolumeInfo(title: "Someone you know", authors: ["Shari Lapena"], publisher: "Some Publisher", argument: "An example argument", pages: 345, releaseDate: .now, googleBooksRating: 4.5)

    static let example2VolumeInfo =
        VolumeInfo(title: "Another Book", authors: ["Shari Lapena"], publisher: "Another Publisher", argument: "Another example argument", pages: 567, releaseDate: .now, googleBooksRating: 5)

    required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: codingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        authors = try container.decode([String].self, forKey: .authors)
        publisher = try container.decode(String.self, forKey: .publisher)
        argument = try container.decode(String.self, forKey: .argument)
        pages = try container.decode(Int.self, forKey: .pages)
        releaseDate = try container.decode(Date.self, forKey: .releaseDate)
        googleBooksRating = try container.decode(Double.self, forKey: .googleBooksRating)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: codingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encodeIfPresent(authors, forKey: .authors)
        try container.encode(publisher, forKey: .publisher)
        try container.encode(argument, forKey: .argument)
        try container.encode(pages, forKey: .pages)
        try container.encode("\(releaseDate)", forKey: .releaseDate)
        try container.encode(googleBooksRating, forKey: .googleBooksRating)
    }
}

struct ContentView: View {

    @Environment(\.modelContext) var modelContext
    @Query private var books: [Book]
    @State private var results = [Book]()

    var body: some View {
        NavigationStack {

            List(results) { book in
                Text (book.aBookInMyLibrary.title)
            }
        }
    }

    func loadData () async {
        guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=authors:shari+lapena&key=MYKEY") else {
            print("Invalid URL")
            return
        }

        do {
            let (data, _) = try await URLSession.shared.data(from: url)

            if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
                results = decodedResponse.results
            }

        } catch {
            print("Invalid data")
        }
    }
}

#Preview {
    ContentView()
}

   

@Kitty is still having trouble. I think my hints were too vague?

JSON isn't easy.

Decoding other people's JSON data is not always easy. Many JSON files tend to look like Russian Stacking ๐ŸŽŽ Dolls.

This is the case with the Google Book JSON. The outer element is response. The response contains an array of Items. Each item contains a volumeInfo. The volumeInfo may contain publisher, or maybe not. Et cetera. Mix in the confusion that you simply want books, but Google calls them volumeInfo. Ugh.

To ease the pain, please study the following solution. I hope this helps you along your path. But you can't learn by simply copying this code. PLEASE take an hour or so and understand each line.

Google Book Response

// These are the structs to handle the responses you'll get from Google books.
// MARK: - Google Response
struct GoogleSearchResponse: Decodable {
    let kind:        String
    let totalItems:  Int
    let items:      [Item]
}

// MARK: - Item
struct Item: Decodable, Identifiable {
    // An item could be a book, or a magazine
    var id:           String { selfLink }
    let kind:         String
    let selfLink:     String
    let volumeInfo:   BookInfo
    // lots o' stuff cut here.
}

// MARK: - VolumeInfo
struct BookInfo: Decodable, Identifiable  {
    var id :        String { self.title ?? "Unk" }
    let title:      String?
    let authors:   [String]?  // Could be several authors!
    let publisher:  String?
    let pageCount:  Int?
    let language:   String?
    // lots o' stuff cut here.
}

Some code to illustrate

import SwiftUI
// Show a LIST of books from Google search.
struct BookListView: View {
    // Initially this is nil, until the search is complete
    @State private var jsonResponse: GoogleSearchResponse?

    var body: some View {
        List {
            //                      ๐Ÿ‘‡๐Ÿผ    Can you think of a better name?
            ForEach( jsonResponse?.items ?? []) { item in
                BookView(book: item.volumeInfo)  // ๐Ÿ‘ˆ๐Ÿผ Am not happy with names here.
            }
        }
        // How fast is your internet? Could have instant results. 
        // Could take a few minutes! ยกSer paciente!
        .task { await loadData() }
    }

    func loadData () async {
        guard let googleBookURL = URL(string: "https://www.googleapis.com/books/v1/volumes?q=authors:stephen+king") else {
            print("Invalid URL ")
            return
        }

        do {
            // @twoStraws added this cool extension       ๐Ÿ‘‡๐Ÿผ
            jsonResponse = try await URLSession.shared.decode(GoogleSearchResponse.self, from: googleBookURL)
        } catch {
            print("Mala suerte!\n Download error: \(error.localizedDescription)")
        }
    }
}

// Show just ONE Book
struct BookView: View {
    let book: BookInfo
    var body: some View {
        VStack(alignment: .leading) {
            HStack { Text(book.title ?? "No Title").bold(); Spacer()
                Text("\(book.pageCount ?? 0) pages").font(.caption2) }
            //                   ๐Ÿ‘‡๐Ÿผ Nice trick to handle multiple authors!
            Text(book.authors?.joined(separator: ", ") ?? "No Author")
                .font(.footnote)
        }
    }
}

// See this article for details!
// https://www.hackingwithswift.com/quick-start/concurrency/how-to-download-json-from-the-internet-and-decode-it-into-any-codable-type
extension URLSession {
    // Super useful extension!
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        //  These are sensible default values.                        ๐Ÿ‘‡๐Ÿผ
        keyDecodingStrategy:  JSONDecoder.KeyDecodingStrategy  = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy  = keyDecodingStrategy
        decoder.dataDecodingStrategy = dataDecodingStrategy
        decoder.dateDecodingStrategy = dateDecodingStrategy

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

Keep Coding!

Please let us know if this helped. Share your questions and learning with us.

1      

I have been taking a long time to understand all of it!!!!!

I understand most of it

The really hardest part for me is the last one. The extension URLSession I haven't arrived there yet.

I have read the article where he explains it. Thanks !!!!!

   

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topicโ€ฆ

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.