Reputation:
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
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