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.
Related
I have a JSON response from my api that returns this:
[
{
"id": 1,
"chapter": 5,
"amount": 28,
"texts": [
{
"lyric": "lorem ipsum",
"number": 1
},
{
"lyric": "lorem foo bar",
"number": 2
}
],
"book": 1
}
]
I tried
struct Chapter: Decodable, Identifiable {
var id: Int
var chapter: Int
var amount: Int
struct Lyrics: Codable {
var lyricText: String
var lyricNumber: Int
}
enum Codingkeys: String, CodingKey {
case lyricText = "lyric"
case lyricNumber = "number"
}
}
But I get the following error upon making the call
dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 0." UserInfo={NSDebugDescription=Invalid value around character 0.})))
My API call looks like this:
...
#Published var chapters = [Chapter]()
func fetchBookDetails() {
if let url = URL(string: url) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
if let safeData = data {
do {
let response = try JSONDecoder().decode([Chapter].self, from: safeData)
DispatchQueue.main.async {
self.chapters = response
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
The struct looks fine I guess, but the api call is complaining - any idea what it could be? Or is it the struct that is done incorrectly
texts is a sub structure (an array of properties), so you need to define a second container for it, for example
struct Text: Codable {
let lyric: String
let number: Int
}
Then you can update Chapter to reference the sub structure something like...
struct Chapter: Decodable {
let id: Int
let chapter: Int
let amount: Int
let book: Int
let texts: [Text]
}
And finally, load it...
let chapters = try JSONDecoder().decode([Chapter].self, from: jsonData)
But what about the error message?
dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Invalid value around character 0." UserInfo={NSDebugDescription=Invalid value around character 0.})))
Oh, right, but the error message is telling there is something wrong with what you've downloaded. I like to, in these cases, convert the data to String and print it if possible, that way, you know what is been returned to you.
For example:
let actualText = String(data: safeData, encoding: .utf8)
The print this and see what you're actually getting
The Playground test code
import UIKit
let jsonText = """
[
{
"id": 1,
"chapter": 5,
"amount": 28,
"texts": [
{
"lyric": "lorem ipsum",
"number": 1
},
{
"lyric": "lorem foo bar",
"number": 2
},
],
"book": 1
}
]
"""
struct Text: Codable {
let lyric: String
let number: Int
}
struct Chapter: Decodable {
let id: Int
let chapter: Int
let amount: Int
let book: Int
let texts: [Text]
}
let jsonData = jsonText.data(using: .utf8)!
do {
let chapters = try JSONDecoder().decode([Chapter].self, from: jsonData)
} catch let error {
error
}
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()
I am new to iOS, and want to parse the JSON using Decodable but cant get through this, how should I work this out?
The view controller where I am trying to parse the data
class ViewController: UIViewController {
var servers = [Server]()
let apiUrl = "https://someurl/api/dashboard"
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: self.apiUrl) else { return }
getDataFrom(url)
}
fileprivate func getDataFrom(_ url: URL) {
URLSession.shared.dataTask(with: url){ (data, response, error) in
guard let data = data else { return }
do {
let apiResponse = try JSONDecoder().decode(Server.self, from: data)
print(apiResponse)
} catch let jsonError {
print(jsonError)
}
}.resume()
}
}
The Server.swift file where I am confirming to the decodable protocol
struct Server: Decodable {
let current_page: Int
let data: [ServerData]
let first_page_url: String
}
struct ServerData: Decodable {
let hostname: String
let ipaddress: String
let customer: [Customer]
let latest_value: [LatestValue]
}
struct Customer: Decodable {
let name: String
let contact_person :String
let email: String
}
struct LatestValue: Decodable {
let systemuptime: String
let memtotal: Float
let memfree: Double
let loadaverage: Float
}
No value associated with key CodingKeys I get this error,
The response from the server
{
"servers": {
"current_page": 1,
"data": [
{
"hostname": "johndoes",
"ipaddress": "10.0.2.99",
"id": 7,
"latest_value_id": 1130238,
"customers": [
{
"name": "Jane Doe",
"contact_person": "John Doe",
"id": 2,
"email": "john.#example.com",
"pivot": {
"server_id": 7,
"customer_id": 2
}
}
],
"latest_value": {
"id": 1130238,
"server_id": 7,
"systemuptime": "80days:10hours:23minutes",
"memtotal": 3.7,
"memfree": 1.6400000000000001,
"loadaverage": 2.25,
"disktotal": {
"dev-mapper-centos-root_disktotal": "38",
"dev-mapper-proquote-xfs-lvm_disktotal": "200"
},
"diskused": "{\"dev-mapper-centos-root_diskused\":\"16\",\"dev-mapper-proquote-xfs-lvm_diskused\":\"188\"}",
"custom_field": "[]",
"additional_attributes": {
"fathom": {
"name": "fathom",
"status": 1
},
"trenddb": {
"name": "trenddb",
"status": 1
},
"trendwi": {
"name": "trendwi",
"status": 1
},
"appsrv": {
"name": "appsrv",
"status": 1
}
},
"created_at": "2019-06-15 02:25:02",
"updated_at": "2019-06-15 02:25:02"
}
}
]
},
"message": "Success"
}
You seem to have few different errors in your data structure.
First of all, you are trying to decode Server while your json has servers inside a dict {"servers": ... }, So use a parent root object for it.
Your latest_value inside ServerData is defined as array, while it should be LatestValue struct not [LatestValue].
There is no first_page_url element in your json, but your Server struct has the property, make it optional, so that JSONDecoder decodes it only if it is present.
Here is your refined data models.
struct Response: Decodable {
let servers: Server
}
struct Server: Decodable {
let current_page: Int
let data: [ServerData]
let first_page_url: String?
}
struct ServerData: Decodable {
let hostname: String
let ipaddress: String
let customers: [Customer]
let latest_value: LatestValue
}
struct Customer: Decodable {
let name: String
let contact_person :String
let email: String
}
struct LatestValue: Decodable {
let systemuptime: String
let memtotal: Float
let memfree: Double
let loadaverage: Float
}
And decode Response instead of decoding Server, like so,
do {
let apiResponse = try JSONDecoder().decode(Response.self, from: data)
let server = apiResponse.server // Here is your server struct.
print(server)
} catch let jsonError {
print(jsonError)
}
I have a following struct with generics for API response with pagination:
struct Paginable<Body> {
let data: [Body]
let meta: Meta
}
extension Paginable {
struct Meta: Codable {
let pagination: Pagination
struct Pagination: Codable {
let total: Int
let count: Int
let perPage: Int
let currentPage: Int
let totalPages: Int
}
}
}
And I want to be able to decode it like so:
let response = try? response.decode(to: Paginable<Segment>.self)
So here's my attempt to make it Decodable:
extension Paginable where Body == Data {
func decode<BodyType: Decodable>(to type: BodyType.Type) throws -> Paginable<BodyType> {
guard let decodedJSON = try? JSONDecoder().decode(BodyType.self, from: data) else {
throw APIError.decodingFailure
}
return Paginable<BodyType>(data: decodedJSON, meta: self.meta)
}
}
This gives me two errors:
Cannot convert value of type 'Paginable.Meta' to expected argument type 'Paginable<_>.Meta'
on the line with return statement
If I change the meta property to some primitive type like Int, the error disappears. But Meta itself is Codable, so what's to problem here?
Cannot convert value of type '[Data]' to expected argument type 'Data'
on the line with guard statement
How to solve this one?
You need to conform Paginable to Codable like,
struct Paginable<Body>: Codable where Body: Codable {
let data: [Body]
let meta: Meta
}
Change decode(data:to:) method in extension Paginable to,
extension Paginable {
static func decode<BodyType: Decodable>(data: Data, to type: BodyType.Type) throws -> BodyType? {
do {
let response = try JSONDecoder().decode(BodyType.self, from: data)
return response
} catch {
throw error
}
}
}
Usage:
if let data = str.data(using: .utf8) {
do {
let response = try Paginable<Segment>.decode(data: data, to: Paginable<Segment>.self)
print(response)
} catch {
print(error)
}
}
Edit:
JSON format:
{
"data": [
{
"name": "Name-1"
},
{
"name": "Name-2"
}
],
"meta": {
"pagination": {
"total": 100,
"count": 10,
"perPage": 5,
"currentPage": 1,
"totalPages": 10
}
}
}
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."
}
*/