Decoding JSON for single object vs array of objects in swift - arrays

I'm fairly new to the swift programming language and have been trying to get this to work for the last week or so. I'm working with an existing API that returns JSON data whose structure changes a little depending on how many venues get returned.
The real structure is somewhat more complicated, but this example illustrates the problem. In one flavor of result, I get a single venue returned like:
{
"totalItems": 21,
"pageSize": 2,
"venues": {
"venue":
{
"name": "Venue Name 1"
"location": "Location A",
"showCount": "4"
}
}
}
In another flavor of result, I get an array of venues returned:
{
"totalItems": 21,
"pageSize": 2,
"venues": {
"venue":
[{
"name": "Venue Name 1"
"location": "Location A",
"showCount": "4"
},
{
"name": "Venue Name 2"
"location": "Location B",
"showCount": "2"
}]
}
}
Yes - the owner of this API should have returned an array regardless, but they didn't and it cannot be changed.
I was able to get this to decode properly for an array of venues (or even if no venues were passed), but it aborts when a single venue is passed (due to the structure variation of course). My code also worked when I changed it to accommodate a single venue, but then returns of multiple venues aborted.
What I'd like to do is decode either variation into an internal structure containing an array regardless of which variation I receive, thus making things far simpler for me to deal with programmatically afterward. Something like this:
struct Response: Decodable {
let totalItems: Int
let pageSize: Int
let venues: VenueWrapper
struct VenueWrapper: Decodable {
let venue: [Venue] // This might contain 0, 1, or more than one venues
}
struct Venue: Decodable {
let name: String
let location: String
let showCount: Int
}
}
Note: In the actual JSON response, there are actually several substructures like this in the response (e.g., a single structure vs an array of structures) which is why I felt simply creating an alternate structure was not a good solution.
I'm hoping someone has come across this before. Thanks in advance!

You can create your own decoder,
struct Response: Decodable {
let totalItems: Int
let pageSize: Int
let venues: VenueWrapper
struct VenueWrapper: Decodable {
var venue: [Venue]
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
venue = []
if let singleVenue = try? values.decode(Venue.self, forKey: CodingKeys.venue) {
//if a single venue decoded append it to array
venue.append(singleVenue)
} else if let multiVenue = try? values.decode([Venue].self, forKey: CodingKeys.venue) {
//if a multi venue decoded, set it as venue
venue = multiVenue
}
enum CodingKeys: String, CodingKey { case venue }
}
}
struct Venue: Decodable {
let name: String
let location: String
let showCount: String
}
}

There's no need for VenueWrapper. 🙅🎁
struct Response {
let totalItems: Int
let pageSize: Int
let venues: [Venue]
struct Venue {
let name: String
let location: String
let showCount: Int
}
}
You'll need to write your own initializer. Unfortunately, I don't think there's a way to eliminate the boilerplate for the not-goofily-encoded properties.
Even if you never need to encode it, making Response Codable, not just Decodable, will give you access to an auto-generated CodingKeys.
extension Response: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
totalItems = try container.decode(Int.self, forKey: .totalItems)
pageSize = try container.decode(Int.self, forKey: .pageSize)
venues = try container.decode(key: .venues)
}
}
That last line relies on a protocol and extension which you can use for any other types that are similarly "encoded". 🙃
protocol GoofilyEncoded: Codable {
/// Must have exactly one case.
associatedtype GoofyCodingKey: CodingKey
}
extension KeyedDecodingContainer {
func decode<Decodable: GoofilyEncoded>(key: Key) throws -> [Decodable] {
let nestedContainer = try self.nestedContainer(
keyedBy: Decodable.GoofyCodingKey.self,
forKey: key
)
let key = nestedContainer.allKeys.first!
do {
return try nestedContainer.decode([Decodable].self, forKey: key)
}
catch {
return try [nestedContainer.decode(Decodable.self, forKey: key)]
}
}
}
All your types that might be encoded in arrays, or not 🤦‍♂️, will need a one-case enum, like so:
extension Response.Venue: GoofilyEncoded {
enum GoofyCodingKey: CodingKey {
case venue
}
}

Related

just one object nested in an array, how to decode the object?

I have this json
{
"status": [
{
"state": "checked",
"errorCode": "123",
"userId": "123456"
}
]
}
this is an array of statuses but is implemented badly because can be just one so I would like to decode just the status object
struct StatusResponse: Codable {
let state: String
let error: String
let id: String
enum CodingKeys: String, CodingKey {
case state = "state"
case error = "errorCode"
case id = "userId"
}
}
I try to custom decode it
let container = try decoder.container(keyedBy: ContainerKeys.self)
var statuses = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .status)
but as expected I get "Expected to decode Dictionary<String, Any> but found an array instead." how I can have access to the first object from statuses variable and decode it into StatusResponse? or some other idea on how to procede?
I would make a struct with field status to represent the top level object. That field is an array of StatusResponse:
struct TopLevelResponse: Codable {
var status: [StatusResponse]
}
when decoding the json:
let decoded = JSONDecoder().decode(TopLevelResponse.self, from: data)
let first = decoded.status.first! // Already decoded!
Unless it's guaranteed that there will be at least one item in the array then you should handle a nil case.
I will go with this solution inspired by this answer:
fileprivate struct RawStatusResponse: Decodable {
let status: [RawStatus]
struct RawStatus: Decodable {
let state: String
let errorCode: String
let userId: String
}
}
struct StatusResponse: Codable {
let state: String
let error: String
let id: String
enum CodingKeys: String, CodingKey {
case state = "state"
case error = "errorCode"
case id = "userId"
}
public init(from decoder: Decoder) throws {
let raw = try RawStatusResponse(from: decoder)
state = raw.status.first!.state
error = raw.status.first!.errorCode
id = raw.status.first!.userId
}
}
then when decode it just decode the actual object:
let state = try JSONDecoder().decode(StatusResponse, from: json)
You could decode it as a dictionary and use flatMap to get the array
let status = try JSONDecoder().decode([String: [StatusResponse]].self, from: data).flatMap(\.value)

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
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
}

How to decode JSON that has multiple lists that are unnamed in Swift [duplicate]

This question already has answers here:
Decoding JSON array of different types in Swift
(2 answers)
Closed 4 years ago.
I am trying to decode a JSON string but it seems to have multiple lists that do not have any names/keys that I can call with my structs. From what I can tell (with the little knowledge I have) there are two lists inside of this JSON and I only want the second list. I know how to decode normal JSON but figuring out how to call this keyless list/array is perplexing.
I tried to make my struct use 0 or 1, depending on which list I wanted, as the case name but that did not work either. I am really just confused on how to call something that is not named explicitly. Below is my JSON data and code.
Here is a small portion of the JSON:
[{
"page": 1,
"pages": 1,
"per_page": "5000",
"total": 58
},
[{
"indicator": {
"id": "NY.GDP.MKTP.CD",
"value": "GDP (current US$)"
},
"country": {
"id": "US",
"value": "United States"
},
"value": "19390604000000",
"decimal": "0",
"date": "2017"
},
{
"indicator": {
"id": "NY.GDP.MKTP.CD",
"value": "GDP (current US$)"
},
"country": {
"id": "US",
"value": "United States"
},
"value": "18624475000000",
"decimal": "0",
"date": "2016"
}]
]
Here is my Swift code:
let url = URL(string:"https://api.worldbank.org/countries/USA/indicators/NY.GDP.MKTP.CD?per_page=5000&format=json")!
let task = URLSession.shared.dataTask(with: url) {(data,response, error) in
if let data = data {
let jsonDecoder = JSONDecoder()
let countryData = try? jsonDecoder.decode(CountryData.self, from:data)
print(countryData?.data)
}
}
task.resume()
struct CountryData: Codable {
let data: [Country]
enum CodingKeys: String, CodingKey {
case data = ""
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
self.data = try valueContainer.decode([Country].self, forKey: CodingKeys.data)
}
}
struct Country: Codable {
let value: String
let date: String
let total: String
enum CodingKeys: String, CodingKey {
case value = "value"
case date = "date"
case total = "total"
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
self.value = try valueContainer.decode(String.self, forKey: CodingKeys.value)
self.date = try valueContainer.decode(String.self, forKey: CodingKeys.date)
self.total = try valueContainer.decode(String.self, forKey: CodingKeys.total)
}
}
extension URL {
func withQueries(_ queries: [String: String]) -> URL? {
var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
components?.queryItems = queries.compactMap
{ URLQueryItem(name: $0.0, value: $0.1) }
return components?.url
}
}
I really just want to eventually access the dates and put them into an array for a tableView and be able to access the rest of the JSON data for the following view.
Thank you so much,
Jack
You simply need to use decoder.unkeyedContainer() when decoding a JSON Array manually. Then you can specify the type of the elements you want to decode one-by-one, which you'll need, since the first and second element of your array are different. If they were the same, you could simply decode it using JSONDecoder.decode([ArrayElementType].self).
struct CountryData: Decodable {
struct Indicator: Decodable {
let id:String
let value:String
}
struct Country: Decodable {
let id:String
let value:String
}
let indicator:Indicator
let country:Country
let value:String
let decimal:String
let date:String
}
struct CountryDataResponse: Decodable {
let countries:[CountryData]
struct CountryDataRoot: Decodable {
let page:Int
let pages:Int
let per_page:String
let total:Int
}
init(from decoder:Decoder) throws {
var container = try decoder.unkeyedContainer()
try container.decode(CountryDataRoot.self)
countries = try container.decode([CountryData].self)
}
}
let countries = try JSONDecoder().decode(CountryDataResponse.self, from: yourJson)

Swift appending to an array with custom objects and sorting them

I am making a collection view that shows somebody their 'feed' which includes their compliments, likes, replies, and sent. I have an object named Feed which is made of an array of compliments, an array of likes, and array of replies, and an array of sent. Every object within those smaller arrays has a date_created property.
I know how to fill a collection view based off of one array sorted by dates, but how can I load a collection view with every like, reply, compliment, and sent from four arrays based on their date sent?
Do I perhaps append every one of these objects to one array? If so, how can I then go through that array and sort everything in it?
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
struct Like: Decodable {
let id: Int
let user: QuickUser
let created_at: String
}
struct Reply: Decodable {
let parent: Compliment
let content: String
let created_at: String
}
struct Compliment: Decodable {
let id: Int
let content: String
let sender: QuickUser
let created_at: String
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
func getFeed() {
UserServices.getInboxFeed { feed in
guard let feed = feed else {
self.showOkayAlert(title: "Oops!", message: "We are having trouble loading your inbox.", completion: nil)
return
}
self.feed = feed
DispatchQueue.main.async {
//load collection view here
}
}
}
A mockup of what the collection view will look like
Well, to solve this i have made a dummy protocol that contains the shared properties they all share.
in this case it was created_at and i assume this should be in date format.
however the next step was to make an extension of Feed that contains a function
that returns the latest object that confirm to the dummy protocol that i called, it LatestAction,
it basically sort each array by date and return the first item.
in our case the latest date,
after that creates a [LatestAction] sort it again the same way and return the first item which is the LatestAction.
Observe the code below.
struct QuickUser: Codable {
}
struct Feed: Decodable {
let likes: [Like]
let replies: [Reply]
let compliments: [Compliment]
let sent: [Compliment]
}
extension Feed {
func latestAction() -> LatestAction {
let latestLike = self.likes.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestReply = self.replies.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestComp = self.compliments.sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending }).first!
let latestActions: [LatestAction] = [latestReply, latestLike, latestComp].sorted(by: { $0.created_at.compare($1.created_at) == .orderedDescending })
return latestActions.first!
}
}
protocol LatestAction {
var created_at: Date { get }
}
struct Like: Decodable, LatestAction {
let id: Int
let user: QuickUser
let created_at: Date
}
struct Reply: Decodable, LatestAction {
let parent: Compliment
let content: String
let created_at: Date
}
struct Compliment: Decodable, LatestAction {
let id: Int
let content: String
let sender: QuickUser
let created_at: Date
let is_liked: Bool
let recipient: QuickUser
let is_public: Bool
}
// Simply used like this
var latest = feed.latestAction()
We are not done yet if you checked the latest type it would say LatestAction and only can access the one property in there which is created_at,
now the for the last part we are going to TypeCast the Object.
if let latest = feed.latestAction() as? Like {
// used as Like
} else if let latest = feed.latestAction() as? Compliment {
// used as Compliment
} else if let latest = feed.latestAction() as? Reply {
// used as reply
}

Swift 4 JSON Codable - value returned is sometimes an object, others an array

the data I'm getting from an API returns a single object but when there's multiple objects, it returns an array in the same key. With the current model (struct) I'm working with, the decoding fails when an array shows up.
These results are randomly ordered, meaning I can't know when I will be served an object or array.
Is there a way to create a model that takes this fact into account and can assign the correct type to cast for the value ('String' or '[String]') so that the decoding continues without problem?
This is an example of when an object is returned:
{
"firstFloor": {
"room": "Single Bed"
}
}
This is an example of when an array is returned (for the same key):
{
"firstFloor": {
"room": ["Double Bed", "Coffee Machine", "TV", "Tub"]
}
}
Example of the struct that should be able to be used as model to decode both samples above:
struct Hotel: Codable {
let firstFloor: Room
struct Room: Codable {
var room: String // the type has to change to either array '[String]' or object 'String' depending on the returned results
}
}
These results are randomly ordered, meaning I can't know when I will be served an object or array.
Here is the complete playground file:
import Foundation
// JSON with a single object
let jsonObject = """
{
"firstFloor": {
"room": "Single Bed"
}
}
""".data(using: .utf8)!
// JSON with an array instead of a single object
let jsonArray = """
{
"firstFloor": {
"room": ["Double Bed", "Coffee Machine", "TV", "Tub"]
}
}
""".data(using: .utf8)!
// Models
struct Hotel: Codable {
let firstFloor: Room
struct Room: Codable {
var room: String // the type has to change to either array '[String]' or object 'String' depending on the results of the API
}
}
// Decoding
let decoder = JSONDecoder()
let hotel = try decoder.decode(Hotel.self, from: jsonObject) //
print(hotel)
You might encapsulate the ambiguity of the result using an Enum with Associated Values (String and Array in this case), for example:
enum MetadataType: Codable {
case array([String])
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .array(container.decode(Array.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .array(let array):
try container.encode(array)
case .string(let string):
try container.encode(string)
}
}
}
struct Hotel: Codable {
let firstFloor: Room
struct Room: Codable {
var room: MetadataType
}
}

Resources