0

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

3
  • 1
    is it correct formated json ?? Commented Apr 22, 2021 at 10:12
  • 1. The data structure is invalid. 2. The key names are incorrect. 3. You have a dictionary, not an array of a dictionary. Commented Apr 22, 2021 at 10:15
  • @EkramulHoque thanks for the reply! No sorry it's just quickly typed from my phone, so it's probably not correct, but the original file i'm using is correct yes! "data" is supposed to be a dictionary, and "books" is just an array of dictionaries. I'm trying to convert it to an array of objects, but using top level data in the Book struct too. Commented Apr 22, 2021 at 10:25

1 Answer 1

2

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
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for all the details, that help a lot!! Yes i was hoping to be able to do it with init(decoder:) and custom encoding with Codable, but maybe its not possible after all..
@Johhn432 I provided a way with custom init(decoder:), but as I suggest, it might be harder to debug later.