How to access JSON using Codable. this is my sample json.
{
"status": "success",
"message": "Data received successfully",
"course": {
"id": 1,
"description": "something",
"name": "ielts",
"attachments": [
{
"id": 809,
"attachment": "https:--",
"file_name": "syllabus.pdf",
"description": "course",
},
{
"id": 809,
"attachment": "https:--",
"file_name": "syllabus.pdf",
"description": "course",
}]
"upcased_name": "IELTS"
}
}
This is my code.
struct ResponseObject: Codable {
let course: [Course]
}
struct Course: Codable {
let id: Int
let name: String
let description: String
let attachments: [Attachments]
}
struct Attachments: Codable {
let id: Int
let attachment: String
let file_name: String
let description: String
let about: String
}
var course: [Course] = []
This is my api call.
func fetchUserData() {
let headers: HTTPHeaders = [
"Authorization": "Token token="+UserDefaults.standard.string(forKey: "auth_token")!,
"Accept": "application/json"
]
let params = ["course_id" : "1"] as [String : AnyObject]
self.showSpinner("Loading...", "Please wait!!")
DispatchQueue.global(qos: .background).async {
AF.request(SMAConstants.courses_get_course_details , parameters: params, headers:headers ).responseDecodable(of: ResponseObject.self, decoder: self.decoder) { response in
DispatchQueue.main.async {
self.hideSpinner()
guard let value = response.value else {
print(response.error ?? "Unknown error")
return
}
self.course = value.course
}
}
}
}
}
I am getting following error.
responseSerializationFailed(reason:
Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error:
Swift.DecodingError.typeMismatch(Swift.Array,
Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue:
"course", intValue: nil)], debugDescription: "Expected to decode
Array but found a dictionary instead.", underlyingError: nil))))
Your model object does not match the JSON structure. Try this instead:
struct ResponseObject: Codable {
let status, message: String
let course: Course
}
struct Course: Codable {
let id: Int
let courseDescription, name: String
let attachments: [Attachment]
let upcasedName: String
enum CodingKeys: String, CodingKey {
case id
case courseDescription = "description"
case name, attachments
case upcasedName = "upcased_name"
}
}
struct Attachment: Codable {
let id: Int
let attachment, fileName, attachmentDescription: String
enum CodingKeys: String, CodingKey {
case id, attachment
case fileName = "file_name"
case attachmentDescription = "description"
}
}
and to download and parse this with plain Swift and Foundation, use code like this:
let url = URL(string: SMAConstants.courses_get_course_details)!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print(error)
return
}
if let data = data {
do {
let response = try JSONDecoder().decode(ResponseObject.self, from: data)
// access your data here
} catch {
print(error)
}
}
}
task.resume()
Related
I have an API response below. The "USER_LIST" response is different based on the value of "DATA_NUM". The problem I have is when the "DATA_NUM" is "0", it returns an empty string AND when "DATA_NUM" is "1", the "USER_LIST" returns both object and an empty string so that I can't decode with a model below. I want to construct a model that's suitable for every case regardless of the value of the "DATA_NUM".
How can I achieve this? Thanks in advance.
API response
// when "DATA_NUM": "0"
{
"RESPONSE": {
"DATA_NUM": "0",
"USER_LIST": ""
}
}
// when "DATA_NUM": "1"
{
"RESPONSE": {
"DATA_NUM": "1",
"USER_LIST": [
{
"USER_NAME": "Jason",
"USER_AGE": "30",
"ID": "12345"
},
""
]
}
}
// when "DATA_NUM": "2"
{
"RESPONSE": {
"DATA_NUM": "2",
"USER_LIST": [
{
"USER_NAME": "Jason",
"USER_AGE": "30",
"ID": "12345"
},
{
"USER_NAME": "Amy",
"USER_AGE": "24",
"ID": "67890"
}
]
}
}
Model
struct UserDataResponse: Codable {
let RESPONSE: UserData?
}
struct UserData: Codable {
let DATA_NUM: String?
let USER_LIST: [UserInfo]?
}
struct UserInfo: Codable {
let USER_NAME: String?
let USER_AGE: String?
let ID: String?
}
Decode
do {
let res: UserDataResponse = try JSONDecoder().decode(UserDataResponse.self, from: data)
guard let userData: UserData = res.RESPONSE else { return }
print("Successfully decoded", userData)
} catch {
print("failed to decode") // failed to decode when "DATA_NUM" is "0" or "1"
}
Here is a solution using a custom init(from:) to handle the strange USER_LIST
struct UserDataResponse: Decodable {
let response : UserData
enum CodingKeys: String, CodingKey {
case response = "RESPONSE"
}
}
struct UserData: Decodable {
let dataNumber: String
let users: [UserInfo]
enum CodingKeys: String, CodingKey {
case dataNumber = "DATA_NUM"
case users = "USER_LIST"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataNumber = try container.decode(String.self, forKey: .dataNumber)
if let _ = try? container.decode(String.self, forKey: .users) {
users = []
return
}
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .users)
var temp: [UserInfo] = []
do {
while !nestedContainer.isAtEnd {
let user = try nestedContainer.decode(UserInfo.self)
temp.append(user)
}
} catch {}
self.users = temp
}
}
struct UserInfo: Decodable {
let name: String
let age: String
let id: String
enum CodingKeys: String, CodingKey {
case name = "USER_NAME"
case age = "USER_AGE"
case id = "ID"
}
}
An example (data1,data2,data3 corresponds to the json examples posted in the question)
let decoder = JSONDecoder()
for data in [data1, data2, data3] {
do {
let result = try decoder.decode(UserDataResponse.self, from: data)
print("Response \(result.response.dataNumber)")
print(result.response.users)
} catch {
print(error)
}
}
Output
Response 0
[]
Response 1
[__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345")]
Response 2
[__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345"), __lldb_expr_93.UserInfo(name: "Amy", age: "24", id: "67890")]
Edit with alternative solution for the while loop
In the above code there is a while loop surrounded by a do/catch so that we exit the loop as soon an error is thrown and this works fine since the problematic empty string is the last element in the json array. This solution was chosen since the iterator for the nestedContainer is not advanced to the next element if the decoding fails so just doing the opposite with the do/catch (where the catch clause is empty) inside the loop would lead to an infinite loop.
An alternative solution that do work is to decode the "" in the catch to advance the iterator. I am not sure if this is needed here but the solution becomes a bit more flexible in case the empty string is somewhere else in the array than last.
Alternative loop:
while !nestedContainer.isAtEnd {
do {
let user = try nestedContainer.decode(UserInfo.self)
temp.append(user)
} catch {
_ = try! nestedContainer.decode(String.self)
}
}
You can write this code to resolve this array string issue.
struct UserDataResponse: Codable {
let RESPONSE: UserData?
}
struct UserData: Codable {
let DATA_NUM: String?
let USER_LIST: [UserInfo]?
struct USER_LIST: Codable {
var USER_LIST: CustomMetadataType
}
}
enum CustomMetadataType: 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(CustomMetadataType.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 UserInfo: Codable {
let USER_NAME: String?
let USER_AGE: String?
let ID: String?
}
I'm trying to parse this JSON response
{
"payload": {
"bgl_category": [{
"number": "X",
"name": "",
"parent_number": null,
"id": 48488,
"description": "Baustellenunterk\u00fcnfte, Container",
"children_count": 6
}, {
"number": "Y",
"name": "",
"parent_number": null,
"id": 49586,
"description": "Ger\u00e4te f\u00fcr Vermessung, Labor, B\u00fcro, Kommunikation, \u00dcberwachung, K\u00fcche",
"children_count": 7
}]
},
"meta": {
"total": 21
}
}
What I'm interested to view in my TableViewCell are only the number and description
here is what I tried to far:
//MARK: - BGLCats
struct BGLCats: Decodable {
let meta : Meta!
let payload : Payload!
}
//MARK: - Payload
struct Payload: Decodable {
let bglCategory : [BglCategory]!
}
//MARK: - BglCategory
struct BglCategory: Decodable {
let descriptionField : String
let id : Int
let name : String
let number : String
let parentNumber : Int
}
//MARK: - Meta
struct Meta: Decodable {
let total : Int
}
API request:
fileprivate func getBgls() {
guard let authToken = getAuthToken() else {
return
}
let headers = [
"content-type" : "application/json",
"cache-control": "no-cache",
"Accept" : "application/json",
"Authorization": "\(authToken)"
]
let request = NSMutableURLRequest(url: NSURL(string: "https://api-dev.com")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
request.allHTTPHeaderFields = headers
let endpoint = "https://api-dev.com"
guard let url = URL(string: endpoint) else { return }
URLSession.shared.dataTask(with: request as URLRequest) {(data, response, error) in
guard let data = data else { return }
do {
let BGLList = try JSONDecoder().decode(BglCategory.self, from: data)
print(BGLList)
DispatchQueue.main.sync { [ weak self] in
self?.number = BGLList.number
self?.desc = BGLList.descriptionField
// self?.id = BGLList.id
print("Number: \(self?.number ?? "Unknown" )")
print("desc: \(self?.desc ?? "Unknown" )")
// print("id: \(self?.id ?? 0 )")
}
} catch let jsonError {
print("Error Serializing JSON:", jsonError)
}
}.resume()
}
But I'm getting error:
Error Serializing JSON: keyNotFound(CodingKeys(stringValue: "childrenCount", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"childrenCount\", intValue: nil) (\"childrenCount\").", underlyingError: nil))
There are a few issues here.
You created the model (mostly) correctly, but there're just two mismatches:
struct BglCategory: Decodable {
let description : String // renamed, to match "description" in JSON
let parentNum: Int? // optional, because some values are null
// ...
}
Second issue is that your model properties are camelCased whereas JSON is snake_cased. JSONDecoder has a .convertFromSnakeCase startegy to automatically handle that. You need to set it on the decoder prior to decoding.
Third issue is that you need to decode the root object BGLCats, instead of BglCategory.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // set the decoding strategy
let bglCats = try decoder.decode(BGLCats.self, from: data) // decode BGLCats
let blgCategories = bglCats.payload.bglCategory
The problem is that JSONDecoder doesn't know that for example bglCategory is represented in JSON payload as bgl_category. If the JSON name isn't the same as the variable name you need to implement CodingKeys to your Decodable
In your case:
struct BglCategory: Decodable {
let descriptionField : String
let id : Int
let name : String
let number : String
let parentNumber : Int?
enum CodingKeys: String, CodingKey {
case id, name, number
case descriptionField = "description"
case parentNumber = "parent_number"
}
}
struct Payload: Decodable {
let bglCategory : [BglCategory]!
enum CodingKeys: String, CodingKey {
case bglCategory = "bgl_category"
}
}
i want to Parsing a json but Xcode Show this message : keyNotFound(CodingKeys(stringValue: "Id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"Id\", intValue: nil) (\"Id\").", underlyingError: nil))
JSON :
{
"result": [
{
"Id": 5,
"Title": "Test1",
"EnTitle": "Story and Novel"
},
{
"Id": 38,
"Title": "Test2",
"EnTitle": " Motivational"
}
],
"status": {
"message": "Confirm",
"success": true,
"systemDateTime": "2019-11-01T12:07:05+03:30",
"httpStatusCode": 200
}
}
Model :
struct Result : Decodable {
let Id : Int
let Title : String
let EnTitle : String
}
class ResultInitialiser {
let Id : Int
let Title : String
let EnTitle : String
init(Id:Int, Title:String, EnTitle: String) {
self.Id = Id
self.Title = Title
self.EnTitle = EnTitle
}
}
View Controller :
var genresFetch = [Result]()
var structGenresFetch = [ResultInitialiser]()
let headers : HTTPHeaders = ["Token" : "6f8652e3-d9d9-4b34-9455-0fa32e82ec58"]
AF.request(BASE_URL!, method: .get, headers: headers).response { (response) in
do {
self.genresFetch = [try JSONDecoder().decode(Result.self, from: response.data!)]
for eachProduct in self.genresFetch {
let recived_Data = ResultInitialiser(Id: eachProduct.Id, Title: eachProduct.Title, EnTitle: eachProduct.EnTitle)
self.structGenresFetch.append(recived_Data)
print(eachProduct.Title)
DispatchQueue.main.async {
self.tvMainApi.reloadData()
}
}
}catch {
print(error)
}
}
JSONDecoder will try to decode the JSON from the root. At the root level, there are only two keys, results and status. The Decodable type that you have passed to it, Result, has neither of those keys.
You need to create another struct:
struct Response {
let results: [Result]
}
And decode this struct instead:
do {
// notice the change to this line
self.genresFetch = try JSONDecoder().decode(Response.self, from: response.data!).results
...
}catch {
print(error)
}
IMO, you don't need the extra class called ResultInitialiser. What's wrong with just using the struct as your model?
"No value associated with key CodingKeys(stringValue: \"Id\", intValue: nil)
The root of the json contains result key only and doesn't have id or the other keys you submit with your Result struct , so You need
self.genresFetch = try JSONDecoder().decode(Root.self, from: response.data!)
print(self.genresFetch.result)
struct Root : Decodable {
let result : [Result]
}
struct Result: Decodable {
let id: Int
let title, enTitle: String
enum CodingKeys: String, CodingKey {
case id = "Id"
case title = "Title"
case enTitle = "EnTitle"
}
}
When I hit my configuration API with Postman I am given the following json response back. In this response the two apiVersion keys are numbers and not strings.
{
"data": {
"availability": {
"auth": true,
"ab": true,
"cd": true
},
"helloWorldConfiguration": {
"apiKey": "abcefg",
"rootUrl": "https://foo",
"apiVersion": 3
},
"fooBarConfiguration": {
"baseUrl": "https://foo",
"apiVersion": 1,
"privateApiPath": "",
"publicApiPath": "dev",
"tokenPath": ""
}
},
"errors": []
}
When I try to decode it it fails with a typeMismatch error. When I output the contents of the response, I see the following which looks fine to me.
data = {
availability = {
auth = 1;
ab = 1;
cd = 1;
};
helloWorldConfiguration = {
apiVersion = 1;
baseUrl = "https://foo";
privateApiPath = "";
publicApiPath = dev;
tokenPath = "";
};
fooBarConfiguration = {
apiKey = abcefg;
apiVersion = 3;
rootUrl = "https://foo";
};
};
errors = (
);
The error given to me indicates that data.helloWorldConfiguration.apiVersion is of type string instead of int. We can see from the original HTTP response I get from Postman that's not the case.
typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), CodingKeys(stringValue: "helloWorldConfiguration", intValue: nil), CodingKeys(stringValue: "apiVersion", intValue: nil)], debugDescription: "Expected to decode Int but found a string/data instead.", underlyingError: nil))
21:17:40 ERROR Unable to decode the response data into a model representation.
My model represents those properties as integers so it would appear that it receives the response and considers those numbers to be strings, which they're not.
public struct ServerConfiguration: Decodable {
let availability: AvailabilityConfiguration
let helloWorldConfiguration: HelloWorldConfiguration
let fooBarConfiguration: FooBarConfiguration
init(availability: AvailabilityConfiguration, helloWorldConfiguration: HelloWorldConfiguration, fooBarConfiguration: FloatSinkConfiguration) {
self.availability = availability
self.helloWorldConfiguration = helloWorldConfiguration
self.fooBarConfiguration = fooBarConfiguration
}
}
public struct FooBarConfiguration: Decodable {
let baseUrl: String
let apiVersion: Int
let privateApiPath: String
let publicApiPath: String
let tokenPath: String
init(baseUrl: String, apiVersion: Int, privateApiPath: String, publicApiPath: String, tokenPath: String) {
self.baseUrl = baseUrl
self.apiVersion = apiVersion
self.privateApiPath = privateApiPath
self.publicApiPath = publicApiPath
self.tokenPath = tokenPath
}
}
public struct AvailabilityConfiguration: Decodable {
let auth: Bool
let ab: Bool
let cd: Bool
init(auth: Bool, ab: Bool, cd: Bool) {
self.auth = auth
self.ab = ab
self.cd = cd
}
}
public struct HelloWorldConfiguration: Codable {
let apiKey: String
let rootUrl: String
let apiVersion: Int
init(apiKey: String, rootUrl: String, apiVersion: Int) {
self.apiKey = apiKey
self.rootUrl = rootUrl
self.apiVersion = apiVersion
}
}
As you can see my apiVersion members are both of type integer along with the json response. What am I doing wrong here? I assume what's happening is Swift is considering the numbers in the json string, regardless of how they're actually represented in the json. Is that the case?
Edit to show utf8 string of Alamofire response data
21:44:06 INFO GET: https:foo/configuration
{
"data" : {
"availability" : {
"auth" : true,
"ab" : true,
"cb" : true
},
"helloWorldConfiguration" : {
"apiKey" : "abcd",
"rootUrl" : "https://foo",
"apiVersion" : "3"
},
"fooBarConfiguration" : {
"baseUrl" : "https://foo",
"apiVersion" : "1",
"privateApiPath" : "",
"publicApiPath" : "dev",
"tokenPath" : "auth/token"
}
},
"errors" : []
}
It would seem that despite the API correctly returning apiVersion as a number, Swift is turning it into a string. Am I decoding it incorrectly?
func getRoute<TResponseData: Decodable>(route:String, completion: #escaping (TResponseData) -> Void) throws {
let headers = try! self.getHeaders(contentType: ContentType.json)
let completeUrl: String = self.getUrl(route: route, requestUrl: nil)
logger.info("GET: \(completeUrl)")
Alamofire.request(
completeUrl,
method: .get,
parameters: nil,
encoding: JSONEncoding.default,
headers: headers)
.validate()
.responseJSON { (response) -> Void in
self.logger.info("GET Response: \(String(describing:response.response?.statusCode))")
switch response.result {
case .success(_):
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom(Date.toFooBarDate)
do {
let result = try decoder.decode(TResponseData.self, from: response.data!)
completion(result)
} catch DecodingError.dataCorrupted(let error) {
self.logger.error(error.underlyingError!)
return
} catch {
print(response.result.value!)
print(error)
self.logger.error("Unable to decode the response data into a model representation.")
return
}
}
}
I checked in my playground and it seems that everything is working fine.To find the real issue i think you are required to provide the real url from where you are getting json and can be checked with alamofire
import Foundation
let json = """
{
"data": {
"availability": {
"auth": true,
"ab": true,
"cd": true
},
"helloWorldConfiguration": {
"apiKey": "abcefg",
"rootUrl": "https://foo",
"apiVersion": 3
},
"fooBarConfiguration": {
"baseUrl": "https://foo",
"apiVersion": 1,
"privateApiPath": "",
"publicApiPath": "dev",
"tokenPath": ""
}
},
"errors": []
}
"""
let data = json.data(using: .utf8)
struct Response : Codable {
let data : Data?
let errors : [String]?
}
struct Availability : Codable {
let auth : Bool?
let ab : Bool?
let cd : Bool?
}
struct Data : Codable {
let availability : Availability?
let helloWorldConfiguration : HelloWorldConfiguration?
let fooBarConfiguration : FooBarConfiguration?
}
struct FooBarConfiguration : Codable {
let baseUrl : String?
let apiVersion : Int?
let privateApiPath : String?
let publicApiPath : String?
let tokenPath : String?
}
struct HelloWorldConfiguration : Codable {
let apiKey : String?
let rootUrl : String?
let apiVersion : Int?
}
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: data!)
print(response)
And here is the response
Response(data: Optional(__lldb_expr_11.Data(availability: Optional(__lldb_expr_11.Availability(auth: Optional(true), ab: Optional(true), cd: Optional(true))), helloWorldConfiguration: Optional(__lldb_expr_11.HelloWorldConfiguration(apiKey: Optional("abcefg"), rootUrl: Optional("https://foo"), apiVersion: Optional(3))), fooBarConfiguration: Optional(__lldb_expr_11.FooBarConfiguration(baseUrl: Optional("https://foo"), apiVersion: Optional(1), privateApiPath: Optional(""), publicApiPath: Optional("dev"), tokenPath: Optional(""))))), errors: Optional([]))
Is there anyone can help me fix my model? It seems it does not match with the JSON from API Response.
JSON response from postman
{
"error_code": 0,
"data": [
{
"kode": "001",
"name": "BANK INDONESIA PUSAT JAKARTA"
},
{
"kode": "002",
"name": "PT. BANK RAKYAT INDONESIA (Persero) Tbk."
},
{
"kode": "003",
"name": "BANK EKSPOR INDONESIA"
}
],
"msg": "OK"
}
Last Model Edited:
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
Still got error like this
Store model using alamofire
private static func performRequest<T:Decodable>(route:APIRouter,
decoder: JSONDecoder = JSONDecoder(), completion:#escaping
(Result<T>)->Void) -> DataRequest {
// Alamofire.request(route).responseJSON {
// response in
// print(response)
// }
return Alamofire.request(route).responseJSONDecodable (decoder:
decoder){ (response: DataResponse<T>) in
//print(response)
completion(response.result)
}
}
data is an array not dictionary
let data:[Bank]
//
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
//
do {
let dic = try JSONDecoder().decode(ObjectBank.self,data)
}
catch {
print(error)
}
The structure of your response is ok in principle which you can see using the following Playground:
import Cocoa
let jsonData = """
{
"error_code": 0,
"data": [
{
"kode": "001",
"name": "BANK INDONESIA PUSAT JAKARTA"
},
{
"kode": "002",
"name": "PT. BANK RAKYAT INDONESIA (Persero) Tbk."
},
{
"kode": "003",
"name": "BANK EKSPOR INDONESIA"
}
],
"msg": "OK"
}
""".data(using: .utf8)!
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
do {
let banks = try JSONDecoder().decode(ObjectBank.self, from: jsonData)
print(banks)
} catch {
print(error)
}
This will parse without error. Since I do not know AlamoFire very well I have to assume that there is something going wrong with the type of your completion closure. It will "somehow" have to guess that you want to parse ObjectBank in order to make any sense of your response.
Maybe you would have an easier time with responseData?