Swift Codable with nested json as string - json

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

Related

JSON decoding fails when response has different types

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

Parsing nested JSON using Decodable in Swift

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

In Swift: How do I decode this JSON (which has variable keys)?

There is an API that supplies JSON data that I would like to use. I've given a summary of the JSON below. At the top level, the key to each record is a unique ID that matches the ID in the record itself. These keys are integers in quotes (starting at 1, unsorted and probably not contiguous).
Reading the JSON isn't a problem. What is the Codable "Response" struct required to receive the data?
if let response = try? JSONDecoder().decode(Response.self, from: data)
The JSON
{
"2546": {
"id": "2546",
"title": "Divis and the Black Mountain"
},
"1": {
"id": "1",
"title": "A la Ronde"
},
"2": {
"id": "2",
"title": "Aberconwy House"
}
}
I had this once also, looks like whoever created this endpoint doesn't really understand how JSON works...
try this out and then just return response.values so you have a list of items
struct Item: Codable {
let id, title: String
}
typealias Response = [String: Item]
Use a more dynamic version of CodingKey. You can read more about it here: https://benscheirman.com/2017/06/swift-json/
Check the section "Dynamic Coding Keys"
The Codable type struct Response should be,
struct Response: Decodable {
let id: String
let title: String
}
Now, parse the json data using [String:Response] instead of just Response like so,
do {
let response = try JSONDecoder().decode([String:Response].self, from: data)
print(response) //["1": Response(id: "1", title: "A la Ronde"), "2546": Response(id: "2546", title: "Divis and the Black Mountain"), "2": Response(id: "2", title: "Aberconwy House")]
} catch {
print(error)
}
You should implement a custom CodingKey, something like that:
struct MyResponse {
struct MyResponseItemKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let id = MyResponseItemKey(stringValue: "id")!
static let title = MyResponseItemKey(stringValue: "title")!
}
struct MyResponseItem {
let id: String
let subItem: MyResponseSubItem
}
struct MyResponseSubItem {
let id: String
let title: String
}
let responseItems: [MyResponseItem]
}
Not sure if the key of each item and the value of id are always equal, that's why there are 2 IDs in MyResponse.
And, of course, MyResponse should conform to Codable:
extension MyResponse: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyResponseItemKey.self)
responseItems = try container.allKeys.map { key in
let containerForKey = try container.nestedContainer(keyedBy: MyResponseItemKey.self, forKey: key)
let id = try containerForKey.decode(String.self, forKey: .id)
let title = try containerForKey.decode(String.self, forKey: .title)
return MyResponseItem(id: key.stringValue, subItem: MyResponseSubItem(id: id, title: title))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MyResponseItemKey.self)
for responseItem in responseItems {
if let key = MyResponseItemKey(stringValue: responseItem.id) {
var subItemContainer = container.nestedContainer(keyedBy: MyResponseItemKey.self, forKey: key)
try subItemContainer.encode(responseItem.subItem.id, forKey: .id)
try subItemContainer.encode(responseItem.subItem.title, forKey: .title)
}
}
}
}
This is how you can use MyResponse:
let jsonString = """
{
"2546": {
"id": "2546",
"title": "Divis and the Black Mountain"
},
"1": {
"id": "1",
"title": "A la Ronde"
},
"2": {
"id": "2",
"title": "Aberconwy House"
}
}
"""
if let dataForJSON = jsonString.data(using: .utf8),
let jsonDecoded = try? JSONDecoder().decode(MyResponse.self, from: dataForJSON) {
print(jsonDecoded.responseItems.first ?? "")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let dataFromJSON = try? encoder.encode(jsonDecoded) {
let jsonEncoded = String(data: dataFromJSON, encoding: .utf8)
print(jsonEncoded ?? "")
}
}

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

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