Combine and nested JSON objects - swift

I have the following model:
struct Book: Codable, Identifiable {
var id: Int
var title: String
var author: String
}
struct BookWrapper: Codable {
var books: [Book]
}
and JSON:
{
"books": [
{
"id": 1,
"title": "Nineteen Eighty-Four: A Novel",
"author": "George Orwell"
}, {
"id": 2,
"title": "Animal Farm",
"author": "George Orwell"
}
],
"errMsg": null
}
I'm trying to grab data using Combine, but cannot find a way how to go around that books array. In case of flat data I would use following:
func fetchBooks() {
URLSession.shared.dataTaskPublisher(for: url)
.map{ $0.data }
.decode(type: [Book].self, decoder: JSONDecoder())
.replaceError(with: [])
.eraseToAnyPublisher()
.receive(on: DispatchQueue.main)
.assign(to: &$books)
}
I tried to use BookWrapper.self, but it doesn't make sense. Is there any elegant way how to solve it?

You can just map the books property of BooksWrapper before it gets to your assign:
func fetchBooks() {
URLSession.shared.dataTaskPublisher(for: url)
.map{ $0.data }
.decode(type: BookWrapper.self, decoder: JSONDecoder())
.replaceError(with: BookWrapper(books: [])) //<-- Here
.map { $0.books } //<-- Here
.receive(on: DispatchQueue.main)
.assign(to: &$books)
}

Related

SwiftyJSON data is only using the first element of my json

I'm using Alamofire and SwiftyJSON and I'm getting my data, but my view is only repeating the data for the first element of the JSON. The data should be Monday, Tuesday, Wednesday, and so on
[![enter image description here][1]][1]
Here's the class I'm using
class observer : ObservableObject {
#Published var datas = [datatype]()
init() {
AF.request("https://api.npoint.io/e667b934a476b8b88745").responseData { (data) in
let json = try! JSON(data: data.data!)
let trainingDay = json["weekExercise"].arrayValue.map
{$0["exercise"].stringValue}
print(trainingDay)
for i in json["weekExercise"]{
self.datas.append(datatype(id: i.1["weeknumber"].intValue,
day: i.1["day"].stringValue,
exercise: i.1["exercise"].stringValue,
dayMiles: i.1["dayMiles"].intValue))
}
}
}
}
My data looks like this:
{
"weeknumber": 1,
"weekExercise": [
{
"day": "Monday",
"dayMiles": 6,
"exercise": "6 miles"
},
{
"day": "Tuesday",
"dayMiles": 9,
"exercise": "12 x 400m WU/CD"
},
{
"day": "Wednesday",
"dayMiles": 0,
"exercise": "Rest"
},
{
"day": "Thursday",
"dayMiles": 6,
"exercise": "6 miles"
},
{
"day": "Friday",
"dayMiles": 6,
"exercise": "6 miles"
},
{
"day": "Saturday",
"dayMiles": 6,
"exercise": "6 miles"
},
{
"day": "Saturday",
"dayMiles": 8,
"exercise": "8 miles"
}
],
"totalWeekMiles": 41,
"planName": "Hanson Method Advance"
}
[1]: https://i.stack.imgur.com/9ZlRy.png?s=256
My suggestion is to take advantage of Alamofire's support of Codable and Combine. SwiftyJSON is outdated and not needed anymore.
import Combine
import Alamofire
struct ExerciseData : Codable, Identifiable {
let id : Int
let weeknumber : Int
let weekExercise : [Exercise]
}
struct Exercise : Codable, Identifiable {
let id = UUID()
let day: String
let dayMiles: Int
let exercise: String
private enum CodingKeys : String, CodingKey { case day, dayMiles, exercise}
}
class Observer : ObservableObject {
private var subscription : AnyCancellable?
#Published var exercises = [Exercise]()
init() {
subscription = AF.request("https://api.npoint.io/e667b934a476b8b88745")
.publishDecodable(type: [ExerciseData].self, queue: .global())
.result()
.receive(on: DispatchQueue.main)
.map{ result -> [Exercise] in
switch result {
case .success(let data) : return data.first?.weekExercise ?? []
case .failure: return []
}
}
.sink(receiveCompletion: { _ in
self.subscription = nil // this breaks the retain cycle
}, receiveValue: { exercises in
self.exercises = exercises
})
}
}
You can even remove Alamofire in favor of the built-in data task publisher
import Combine
class Observer : ObservableObject {
private var subscription : AnyCancellable?
#Published var exercises = [Exercise]()
init() {
let url = URL(string: "https://api.npoint.io/e667b934a476b8b88745")!
subscription = URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.map(\.data)
.decode(type: [ExerciseData].self, decoder: JSONDecoder())
.map{$0.first?.weekExercise ?? []}
.replaceError(with: [])
.sink(receiveCompletion: { _ in
self.subscription = nil
}, receiveValue: { exercises in
self.exercises = exercises
})
}
}
The error handling is rudimentary. It returns an empty array in case of an error.

Decoding a generic decodable type

I need to create a generic struct that will hold any decodable type which is returned from the network, so I created something like this:
struct NetworkResponse<Wrapped: Decodable>: Decodable {
var result: Wrapped
}
so I can use the decoding method like this:
struct MyModel: Decodable {
var id: Int
var name: String
var details: String
}
func getData<R: Decodable>(url: URL) -> AnyPublisher<R, Error>
URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: NetworkResponse<R>.self, decoder: decoder)
.map(\.result)
.eraseToAnyPublisher()
//call site
let url = URL(string: "https://my/Api/Url")!
let models: [MyModel] = getData(url: url)
.sink {
//handle value here
}
But, I noticed that some responses from the network contains the result key, and some others do not:
with result:
{
"result": { [ "id": 2, "name": "some name", "details": "some details"] }
}
without result:
[ "id": 2, "name": "some name", "details": "some details" ]
this results in the following error from the .map(\.result) publisher because it can't find the result key in the returned json:
(typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)))
How can I handle either case in the NetworkResponse struct in order to avoid such error?
The JSON your posted isn't valid, but I'm assuming it's a typo and it's actually:
{ "id": 2, "name": "some name", "details": "some details" }
// or
{ "result": { "id": 2, "name": "some name", "details": "some details" } }
({ } instead of [ ])
Probably the cleanest is with a manual decoder that can fall back to another type, if the first type fails:
struct NetworkResponse<Wrapped> {
let result: Wrapped
}
extension NetworkResponse: Decodable where Wrapped: Decodable {
private struct ResultResponse: Decodable {
let result: Wrapped
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let result = try container.decode(ResultResponse.self)
self.result = result.result
} catch DecodingError.keyNotFound, DecodingError.typeMismatch {
self.result = try container.decode(Wrapped.self)
}
}
}
Alternatively, you can fall back within Combine. I would not have gone with this approach, but for completeness-sake:
URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.flatMap { data in
Just(data)
.decode(type: NetworkResponse<R>.self, decoder: decoder)
.map(\.result)
.catch { _ in
Just(data)
.decode(type: R.self, decoder: decoder)
}
}
.eraseToAnyPublisher()

Swift - Constructing a base Decodable struct with Generics

So, I am trying to build a model that will be responsible for URLRequests and parsing Decodables. The response that is coming from the server is in the same form at the highest scope including keys status, page_count and results.
results values are changing respecting to the request, page_count is optional and status is just a String indicating whether the request was successful or not.
I tried to implement Generics to method itself and base Decodable struct named APIResponse and below is an example of just one endpoint, named extList. The code compiles, however in the runtime it throws
Thread 7: Fatal error: 'try!' expression unexpectedly raised an error:
Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "results",
intValue: nil), Swift.DecodingError.Context(codingPath: [],
debugDescription: "No value associated with key
CodingKeys(stringValue: \"results\", intValue: nil) (\"results\").",
underlyingError: nil))
at the line of json = try! JSONDecoder().decode(APIResponse<T>.self, from: data)
class NetworkManager {
typealias completion<T: Decodable> = (Result<APIResponse<T>, Error>)->()
class func make<T: Decodable>(of type: T.Type,
request: API,
completion: #escaping completion<T>){
let session = URLSession.shared
var req = URLRequest(url: request.url)
req.httpMethod = request.method
req.httpBody = request.body
session.dataTask(with: req) { (data, response, error) in
if let error = error {
completion(.failure(error))
return
}
var json: APIResponse<T>
if let data = data,
let response = response as? HTTPURLResponse?,
response?.statusCode == 200 {
switch request {
case .extList:
json = try! JSONDecoder().decode(APIResponse<T>.self, from: data)
default:
return
}
completion(.success(json))
}
}.resume()
}
}
Here is the base struct
struct APIResponse<T: Decodable>: Decodable {
var status: String
var page_count: Int?
var results: T
}
Here is the response that should fill the results key in the APIResponse for this endpoint.
struct UserResponse: Decodable {
var name: String
var extens: Int
}
I am making my request as NetworkManager.make(of: [UserResponse].self, request: .extList) { (result) in ; return } and it works when I discard the Response generic type in the APIResponse with the Array<UserResponse> directly.
As requested, sample json I am trying to decode
{
"status": "ok-ext_list",
"page_count": 1,
"results": [
{
"name": "some name",
"extens": 249
},
{
"name": "some other name",
"extens": 245
}
]
}
Any ideas to fix this?
MINIMAL REPRODUCIBLE EXAMPLE
So the below code is working and I absolutely do not know why.
JSON's
import Foundation
var extListJSON : Data {
return try! JSONSerialization.data(withJSONObject: [
"status": "ok_ext-list",
"page_count": 1,
"results": [
[
"name": "some name",
"extens": 256
],
[
"name": "some other name",
"extens": 262
]
]
], options: .fragmentsAllowed)
}
var extListString: Data {
return """
{
"status": "ok-ext_list",
"page_count": 1,
"results": [
{
"name": "some name",
"extens": 249
},
{
"name": "some other name",
"extens": 245
}
]
}
""".data(using: .utf8)!
}
Manager and Service
enum Service {
case extList
}
class NetworkManager {
typealias completion<T: Decodable> = (APIResponse<T>) -> ()
class func make<T: Decodable>(of type: T.Type, request: Service, completion: completion<T>) {
let data = extListString
let json: APIResponse<T>
switch request {
case .extList:
json = try! JSONDecoder().decode(APIResponse<T>.self, from: data)
}
completion(json)
}
}
Decodables
struct APIResponse<T: Decodable>: Decodable {
var status: String
var page_count: Int?
var results: T
}
struct UserResponse: Decodable {
var name: String
var extens: Int
}
Finally method call
NetworkManager.make(of: [UserResponse].self, request: .extList) { (result) in
dump(result)
}
Again, I have no clue why this is working. I just removed the networking part and it started to work. Just a reminder that my original code is working as well if I just use seperate Decodable for each request -without using Generic struct-. Generic make(:_) is working fine as well.

Swift Codable: decode dictionary with unknown keys

Codable is great when you know the key formatting of the JSON data. But what if you don't know the keys? I'm currently faced with this problem.
Normally I would expect JSON data to be returned like this:
{
"id": "<123>",
"data": [
{
"id": "<id1>",
"event": "<event_type>",
"date": "<date>"
},
{
"id": "<id2>",
"event": "<event_type>",
"date": "<date>"
},
]
}
But this is what I'm aiming to decode:
{
"id": "123",
"data": [
{ "<id1>": { "<event>": "<date>" } },
{ "<id2>": { "<event>": "<date>" } },
]
}
Question is: how do I use Codable to decode JSON where the keys are unique? I feel like I'm missing something obvious.
This is what I'm hoping to do so I can use Codable:
struct SampleModel: Codable {
let id: String
let data: [[String: [String: Any]]]
// MARK: - Decoding
enum CodingKeys: String, CodingKey {
case id = "id"
case data = "data"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
// This throws an error: Ambiguous reference to member 'decode(_:forKey:)'
data = try container.decode([[String: [String: Any]]].self, forKey: .data)
}
}
This throws an error: Ambiguous reference to member 'decode(_:forKey:)'
For your completely changed question, the solution is very similar. Your struct simply adds one additional layer above the array. There's no need for any custom decoding nor even any CodingKeys.
Note that you can't use Any in a Codable.
let json="""
{
"id": "123",
"data": [
{ "<id1>": { "<event>": "2019-05-21T16:15:34-0400" } },
{ "<id2>": { "<event>": "2019-07-01T12:15:34-0400" } },
]
}
"""
struct SampleModel: Codable {
let id: String
let data: [[String: [String: Date]]]
}
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let res = try decoder.decode(SampleModel.self, from: json.data(using: .utf8)!)
print(res)
} catch {
print(error)
}
The original answer for your original question.
Since you have an array of nested dictionary where none of the dictionary keys are fixed, and since there are no other fields, you can just decode this as a plain array.
Here's an example:
let json="""
[
{ "<id1>": { "<event>": "2019-07-01T12:15:34-0400" } },
{ "<id2>": { "<event>": "2019-05-21T17:15:34-0400" } },
]
"""
var decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let res = try decoder.decode([[String: [String: Date]]].self, from: json.data(using: .utf8)!)
print(res)
} catch {
print(error)
}

Swift 4 decoding json using Codable

Can someone tell me what I'm doing wrong? I've looked at all the questions on here like from here How to decode a nested JSON struct with Swift Decodable protocol? and I've found one that seems exactly what I need Swift 4 Codable decoding json.
{
"success": true,
"message": "got the locations!",
"data": {
"LocationList": [
{
"LocID": 1,
"LocName": "Downtown"
},
{
"LocID": 2,
"LocName": "Uptown"
},
{
"LocID": 3,
"LocName": "Midtown"
}
]
}
}
struct Location: Codable {
var data: [LocationList]
}
struct LocationList: Codable {
var LocID: Int!
var LocName: String!
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "/getlocationlist")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
do {
let locList = try JSONDecoder().decode(Location.self, from: data)
print(locList)
} catch let error {
print(error)
}
}
task.resume()
}
The error I am getting is:
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath:
[], debugDescription: "Expected to decode Array but found a
dictionary instead.", underlyingError: nil))
Check the outlined structure of your JSON text:
{
"success": true,
"message": "got the locations!",
"data": {
...
}
}
The value for "data" is a JSON object {...}, it is not an array.
And the structure of the object:
{
"LocationList": [
...
]
}
The object has a single entry "LocationList": [...] and its value is an array [...].
You may need one more struct:
struct Location: Codable {
var data: LocationData
}
struct LocationData: Codable {
var LocationList: [LocationItem]
}
struct LocationItem: Codable {
var LocID: Int!
var LocName: String!
}
For testing...
var jsonText = """
{
"success": true,
"message": "got the locations!",
"data": {
"LocationList": [
{
"LocID": 1,
"LocName": "Downtown"
},
{
"LocID": 2,
"LocName": "Uptown"
},
{
"LocID": 3,
"LocName": "Midtown"
}
]
}
}
"""
let data = jsonText.data(using: .utf8)!
do {
let locList = try JSONDecoder().decode(Location.self, from: data)
print(locList)
} catch let error {
print(error)
}
After searching lots of thing internet, I certainly figured out this is the sweetest way to print well formatted json from any object.
let jsonString = object.toJSONString(prettyPrint: true)
print(jsonString as AnyObject)
Apple documentation about JSONEncoder ->
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(pear)
print(String(data: data, encoding: .utf8)!)
/* Prints:
{
"name" : "Pear",
"points" : 250,
"description" : "A ripe pear."
}
*/