user15102768
user15102768

Reputation:

Decode in Swift a JSON response to an array of struct using top level data

I have a JSON response from an API that looked like this:

{ 
"data": {
    "author_id": "wxyz",
    "author_name": "Will",
    "language": "English",
    "books": [
        {"book_id":"abc1", "book_name":"BookA"},
        {"book_id":"def2", "book_name":"BookB"},
        {"book_id":"ghi3", "book_name":"BookC"}
    ]
  }
}

Currently my Book structs looks like this:

struct Book: Codable {
    let book_id: String
    let book_name: String
}

But I would like to have a Book like this (with data from top level):

struct Book: Codable {
    let book_id: String
    let book_name: String
    let author: Author
    let language: String
}

Using Codable (/decoding custom types), how can I transform the JSON response above directly to a list of books (while some of the data comes from top level object)?

When I use decode I can automatically decode the books array to an array of Book:

let books = try decoder.decode([Book].self, from: jsonData)

But I cannot find the way to pass the author name and id, or the language because it's in the top level

Upvotes: 0

Views: 1323

Answers (1)

Larme
Larme

Reputation: 26096

You might be able to do so with a custom init(decoder:), but another way is to hide the internal implementation where you stick to the JSON Model, and use lazy var or computed get. Lazy var will be load only once, it depends if you keep or not the root.

struct Root: Codable {
    //Hidde,
    private let data: RootData

    //Hidden
    struct RootData: Codable {
        let author_id: String
        let author_name: String

        let language: String

        let books: [RootBook]
    }
    //Hidden
    struct RootBook: Codable {
        let book_id: String
        let book_name: String
    }

    lazy var books: [Book] = {
        let author = Author(id: data.author_id, name: data.author_name)
        return data.books.map {
            Book(id: $0.book_id, name: $0.book_name, author: author, language: data.language)
        }
    }()

    var books2: [Book] {
        let author = Author(id: data.author_id, name: data.author_name)
        return data.books.map {
            Book(id: $0.book_id, name: $0.book_name, author: author, language: data.language)
        }
    }
}

//Visible
struct Book: Codable {
    let id: String
    let name: String
    let author: Author
    let language: String
}
//Visible
struct Author: Codable {
    let id: String
    let name: String
}

Use:

do {
    var root = try JSONDecoder().decode(Root.self, from: jsonData)
    print(root)
    print("Books: \(root.books)") //Since it's a lazy var, it need to be mutable
} catch {
    print("Error: \(error)")
}

or

do {
    let root = try JSONDecoder().decode(Root.self, from: jsonData)
    print(root)
    print("Books: \(root.books2)")
} catch {
    print("Error: \(error)")
}

Side note: The easiest way is to stick to the JSON Model indeed. Now, it might be interesting also to have internal model, meaning, your have your own Book Class, that you init from Root. Because tomorrow, the JSON might change (change of server, etc.). Then the model used for your views (how to show them) might also be different... Separate your layers, wether you want to use MVC, MVVM, VIPER, etc.

EDIT:

You can with an override of init(decoder:), but does it make the code clearer? I found it more difficult to write than the previous version (meaning, harder to debug/modify?)

struct Root2: Codable {
    let books: [Book2]

    private enum TopLevelKeys: String, CodingKey {
        case data
    }
    private enum SubLevelKeys: String, CodingKey {
        case books
        case authorId = "author_id"
        case authorName = "author_name"
        case language
    }
    private enum BoooKeys: String, CodingKey {
        case id = "book_id"
        case name = "book_name"
    }
    init(from decoder: Decoder) throws {
        let topContainer = try decoder.container(keyedBy: TopLevelKeys.self)
        let subcontainer = try topContainer.nestedContainer(keyedBy: SubLevelKeys.self, forKey: .data)
        var bookContainer = try subcontainer.nestedUnkeyedContainer(forKey: .books)
        var books: [Book2] = []
        let authorName = try subcontainer.decode(String.self, forKey: .authorName)
        let authorid = try subcontainer.decode(String.self, forKey: .authorId)
        let author = Author(id: authorid, name: authorName)
        let language = try subcontainer.decode(String.self, forKey: .language)
        while !bookContainer.isAtEnd {
            let booksubcontainer = try bookContainer.nestedContainer(keyedBy: BoooKeys.self)
            let bookName = try booksubcontainer.decode(String.self, forKey: .name)
            let bookId = try booksubcontainer.decode(String.self, forKey: .id)
            books.append(Book2(book_id: bookId, book_name: bookName, author: author, language: language))
        }
        self.books = books
    }
}
struct Book2: Codable {
    let book_id: String
    let book_name: String
    let author: Author
    let language: String
}

Upvotes: 2

Related Questions