Parsing nested JSON using Decodable in Swift - json

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

Related

Parsing Dynamic Json in Swift

{
"AAPL" : {
"quote": {...},
"news": [...],
"chart": [...]
},
"FB" : {
"quote": {...},
"news": [...],
"chart": [...]
},
}
How would you decode this in swift. The stocks change but the underlying quote, news, and chart stay the same. Also to mention this json of stocks could be 500 long with unknown sorting order.
For the information in quote it would look like:
{
"calculationPrice": "tops",
"open": 154,
ect...
}
inside news:
[
{
"datetime": 1545215400000,
"headline": "Voice Search Technology Creates A New Paradigm For
Marketers",
ect...
}
]
Inside charts:
[
{
"date": "2017-04-03",
"open": 143.1192,
ect...
}
]
What I have been trying is something along the lines of this as an example...
Json Response:
{
"kolsh" : {
"description" : "First only brewed in Köln, Germany, now many American brewpubs..."
},
"stout" : {
"description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
}
}
Struct/Model for codable:
struct BeerStyles : Codable {
struct BeerStyleKey : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let description = BeerStyleKey(stringValue: "description")!
}
struct BeerStyle : Codable {
let name: String
let description: String
}
let beerStyles : [BeerStyle]
}
Decoder:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: BeerStyleKey.self)
var styles: [BeerStyle] = []
for key in container.allKeys {
let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
forKey: key)
let description = try nested.decode(String.self,
forKey: .description)
styles.append(BeerStyle(name: key.stringValue,
description: description))
}
self.beerStyles = styles
}
This example is from https://benscheirman.com/2017/06/swift-json/ and I'm trying to apply it to my json structure.
Try this code ...:)
Alamofire.request("", method: .get, encoding: JSONEncoding.default) .responseJSON { response in
if response.result.isSuccess{
let json = response.result.value! as? [String : Any] ?? [:]
for (key, value) in json {
//here key will be your apple , fb
let valueofkey = value as? [String:Any] ?? [:]
let quote = valueofkey["quote"] as? [String:Any] ?? [:]
let news = valueofkey["news"] as? [Any] ?? []
let chart = valueofkey["chart"] as? [Any] ?? []
}
}
}
I hope it will work for you ... :)
If the contents of quote, news and chart have same type, i.e. assuming that quote is of type [String:String] and news and chart are of type [String], you can use Codable as well.
Example:
With the below model,
struct Model: Decodable {
let quote: [String:String]
let news: [String]
let chart: [ String]
}
Now, you can parse the JSON like so,
do {
let response = try JSONDecoder().decode([String:Model].self, from: data)
print(response)
} catch {
print(error)
}

Parsing Json Show Error keyNotFound Swift

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

Swift IOS Failed to access json array data using Codable

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()

Swift Codable with nested json as string

The services I use for backend calls returns all this json-structure:
{
"status" : "OK",
"payload" : **something**
}
Where something can be a simple string:
{
"status" : "OK",
"payload" : "nothing changed"
}
or a nested json (any json with any properties), for example:
{
"status" : "OK",
"payload" : {
"someInt" : 2,
"someString" : "hi",
...
}
}
This is my struct:
struct GenericResponseModel: Codable {
let status:String?
let payload:String?
}
I want to decode "payload" always as a string. So in the second case I want that the payload property of my "GenericResponseModel" contains the json string of that field, but If I try to decode that response I get the error:
Type 'String' mismatch: Expected to decode String but found a dictionary instead
Is possible to archive what I want?
Many thanks
How about this…
Declare a PayloadType protocol…
protocol PayloadType: Decodable { }
and make String, and struct Payload conform to it…
extension String: PayloadType { }
struct Payload: Decodable, PayloadType {
let someInt: Int
let someString: String
}
Then make GenericResponseModel generic…
struct GenericResponseModel<T: PayloadType>: Decodable {
let status: String
let payload: T
enum CodingKeys: CodingKey {
case status, payload
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String.self, forKey: .status)
payload = try container.decode(T.self, forKey: .payload)
}
}
Then you can decode as follows…
let data = """
{
"status" : "OK",
"payload" : "nothing changed"
}
""".data(using: .utf8)!
print(try JSONDecoder().decode(GenericResponseModel<String>.self, from: data))
// GenericResponseModel<String>(status: "OK", payload: "nothing changed")
and
let data2 = """
{
"status" : "OK",
"payload" : {
"someInt" : 2,
"someString" : "hi"
}
}
""".data(using: .utf8)!
print(try JSONDecoder().decode(GenericResponseModel<Payload>.self, from: data2))
// GenericResponseModel<Payload>(status: "OK", payload: Payload(someInt: 2, someString: "hi"))
Of course, this relies on you knowing the payload type in advance. You could get around this by throwing a specific error if payload is the wrong type…
enum GenericResponseModelError: Error {
case wrongPayloadType
}
and then…
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String.self, forKey: .status)
do {
payload = try container.decode(T.self, forKey: .payload)
} catch {
throw GenericResponseModelError.wrongPayloadType
}
}
Then handle this error when you decode…
let data = """
{
"status" : "OK",
"payload" : {
"someInt" : 2,
"someString" : "hi"
}
}
""".data(using: .utf8)!
do {
let response = try JSONDecoder().decode(GenericResponseModel<String>.self, from: data) // Throws
print(response)
} catch let error as GenericResponseModelError where error == .wrongPayloadType {
let response = try JSONDecoder().decode(GenericResponseModel<Payload>.self, from: data2) // Success!
print(response)
}

typeMismatch on decodable Type when the type is correct

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([]))