How to use a dictionary in `codable` response? - ios

I have the following response from a JSON query. How do I represent a dictionary as codable? I have shortened the JSON response to save space.
{
"result":[
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://foo.com/api/now/table/cmn_location/753ac76f4fd9320066bfb63ca310c79b",
"value": "753ac76f4fd9320066bfb63ca310c79b"
}
}
]
}
struct ResultList : Codable {
let result: [Result]
}
struct Result : Codable {
let delivery_address: String
let made_sla: String
let watch_list: String
let upon_reject: String
let location: Location
}
struct Location: Codable {
let link: String?
let value: String?
}
let decoder = JSONDecoder()
do {
let todo = try decoder.decode(ResultList.self, from: responseData)
print("todo \(todo)")
} catch {
print("error trying to convert data to JSON")
print(error)
}
I'm getting the following error:
"Expected to decode Dictionary<String, Any> but found a string/data instead.", underlyingError: nil))

Based on all the comments, I believe the JSON you're actually decoding looks more like this:
{
"result": [{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://foo.com/api/now/table/cmn_location/753ac76f4fd9320066bfb63ca310c79b",
"value": "753ac76f4fd9320066bfb63ca310c79b"
}
},
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": ""
}
]
}
So there are some records that have a location, and some that encode location as an empty string. Basically this is really messed up JSON on a whole lot of levels and whatever code generates it should be fixed. The bad news I'm sure that won't happen. The good news is we can fix it locally at least.
We're going to have to decode this by hand, so we might as well clean up all the rest of the mess while we're in here. The first thing is that the names don't match Swift naming conventions. We can fix that using CodingKeys. We can also provide real types (Bool and a Rejection enum) for the things that are currently mis-typed strings.
enum Rejection: String, Codable {
case cancel
}
struct Result : Codable {
let deliveryAddress: String
let madeSLA: Bool
let watchList: String
let uponReject: Rejection
let location: Location?
private enum CodingKeys: String, CodingKey {
case deliveryAddress = "delivery_address"
case madeSLA = "made_sla"
case watchList = "watch_list"
case uponReject = "upon_reject"
case location
}
}
Now we just need to be able to decode it. Notice that I made Location optional. Clearly it sometimes doesn't exist, so you either need a default value or it needs to be optional. I chose the latter. Decoding all of this is very straight forward:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
deliveryAddress = try container.decode(String.self, forKey: .deliveryAddress)
madeSLA = try container.decode(String.self, forKey: .madeSLA) == "true"
watchList = try container.decode(String.self, forKey: .watchList)
uponReject = try container.decode(Rejection.self, forKey: .uponReject)
location = try? container.decode(Location.self, forKey: .location)
}
The last line is your actual question. It just says if we can't decode it as a Location, set it to nil. We could be stricter here and try first to decode it as a Location, and then as a String, and then check if the String is empty, but it feels reasonable to use nil here for any decoding failure.

Assuming your JSON is (please note the missing closing brace)
{
"result": [
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://blah/blah/foo",
"value": "fsfdfr32r34rwfwffas"
}
}
]
}
You can decode these structs
struct Root : Decodable {
let result : [Result]
struct Result : Decodable {
let location: Location
}
}
struct Location: Decodable {
let link: String
let value: String
}
with
JSONDecoder().decode(Root.self, from: data)

Related

Swift5 : How to handle this JSON response into Swift Data Model

I am very new to Swift, so please pardon me if it is a silly question
How to handle this type of response into data model ,response is in this form:
{
"id" = 1;
count = "";
data = "[{"key":"value","key":"value".......}]";
message = SUCCESS;
"response_code" = 1;
}
The AlamofireRequest gets the response and prints it but when using responseDecodable,nothing happens, the code of Alamofire request is below:
let request = AF.request(urlString!,method: .get)
request.responseJSON { (data) in
print(data)
}
request.responseDecodable(of: Test.self) { (response) in
guard let getData = response.value else {return}
print(getData.all[0].firstName) //Nothing prints
}
And this is how the data model looks like:
struct Test: Decodable {
let firstName: String
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
//firstname is inside data of json
}
}
struct Getdata: Decodable {
let all : [Test]
enum CodingKeys: String, CodingKey {
case all = "data"
}
}
Want to access values inside data and print it. Please shed some light on it!
Trying to address both the question and the comments above, the first thing would be to get some valid JSON. It could be that your API is providing non-JSON data, in which case neither this answer or using AlamoFire to decode it will work. However, formatting the above as JSON, at my best guess of what it's meant to be, will give:
let json = """
{
"id": 1,
"count": "",
"data": [
{
"key1": "value1",
"key2": "value2"
}
],
"message": "SUCCESS",
"response_code": 1
}
"""
At this point it's more obvious that the content under the data key is not an array of Test as defined above, but is an array of dictionaries (with a single enry in the array in the example). So it will be necessary to respecify the data model. For something this simple, for the use case mentioned, there is no real reason to go for multiple types, so we're going to redefine Getdata (I'm sticking with your naming despite not liking it):
struct Getdata: Decodable {
let all : [[String:String]]
enum CodingKeys: String, CodingKey {
case all = "data"
}
}
To test the decoding let's used the standard JSONDecoder (I don't have a Playground handy with AlamoFire as I'd never use it):
do {
let wrapper = try JSONDecoder().decode(Getdata.self, from: Data(json.utf8))
print(wrapper.all)
} catch {
print("Decoding error \(error.localizedDescription)")
}
This outputs
[["key1": "value1", "key2": "value2"]]
as would be expected.
Now there's a valid JSON decoding solution it should be easy to drop this into the AlamoFire APIs if you so wish. Although if the backend really is providing the misformed JSON as in question this won't work and you'll have to change the backend, decode it with your own decoder or mangle it into valid JSON.
Did lot of silly mistakes in original question, long way to go
As suggested by #flanker I did respecify the data model to
struct Test : Codable {
let id : String?
let message : String?
let data : String?
let count : String?
let response_code : String?
enum CodingKeys: String, CodingKey {
case id = "$id"
case message = "message"
case data = "data"
case count = "count"
case response_code = "response_code"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id)
message = try values.decodeIfPresent(String.self, forKey: .message)
data = try values.decodeIfPresent(String.self, forKey: .data)
count = try values.decodeIfPresent(String.self, forKey: .count)
response_code = try values.decodeIfPresent(String.self, forKey: .response_code)
}
And now it seems to work
Now getting the desired output
[["key1": "value1", "key2": "value2"]]

Values of Array of struct are nil

I am pretty new to Swift and as an exercise I try to create an App with Swift 5, which should show you the Weather at a Location you searched for.
Right now I am trying to implement a function that can turn a local JSON file in a struct I am using.
The JSON file is located at the same directory as all the other files.
The JSON for testing purposes looks like this:
[
{
"id": 833,
"name": "Ḩeşār-e Sefīd",
"state": "",
"country": "IR",
"coord": {
"lon": 47.159401,
"lat": 34.330502
}
},
{
"id": 2960,
"name": "‘Ayn Ḩalāqīm",
"state": "",
"country": "SY",
"coord": {
"lon": 36.321911,
"lat": 34.940079
}
}
]
The struct:
struct Root: Codable {
let jsonWeather: [JsonWeather]?
}
struct JsonWeather: Codable {
let id: Int
let name: String
let state: String
let country: String
let coord: Coord
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case state = "state"
case country = "country"
case coord = "coord"
}
}
The function that i am working on:
func loadJson(fileName: String) -> Void{
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let jsonData = try JSONDecoder().decode(Array<Root>.self, from: data)
print(type(of: jsonData)) // Array<Root>
print(jsonData) // [WeatherApp.Root(jsonWeather: nil), WeatherApp.Root(jsonWeather: nil)]
} catch {
print("error:\(error)")
}
}
}
After all I wanted to see how the result looks like and I noticed that each jsonWeather in Root is nil
So in the end I understand that the values are nil because I allow them to be nil by setting an optional in the Root struct, but I dont quite understand why they are turning nil because there is data given. Also I don't know how I would implement it without being an optional.
This is my first time using JSONDecoder and Optionals in Swift.
Could anyone point out what I did wrong (or understood wrong) and how to fix this issue
Since your JSON is just of array of objects, you don't need a top-level struct like Root.
Get rid of your Root struct, and just decode an array of JsonWeathers:
let jsonData = try JSONDecoder().decode([JsonWeather].self, from: data)
(Note that there is no difference between [JsonWeather].self and Array<JsonWeather>.self - I just chose to do it this way since it's shorter.)

how to create dynamic struct with optional generic type

I have created a Decodable base Struct for API response.
struct ResponseBaseModel<T: Decodable>: Decodable {
let status: Bool
let message: String
var result: T?
private enum CodingKeys: String, CodingKey {
case result, message, success,status,statusCode
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
if let result = try? values.decode(T.self, forKey: .result) {
self.result = result
}
status = try values.decode(Bool.self, forKey: .status)
message = try values.decode(String.self, forKey: .message)
}
}
// here is API response
{
"status": true,
"statusCode": 200,
"message": "Theater list successfully",
"result": [
{
"id": 1,
"name": "Galaxy",
"picture": "https://ctdemo.workpc.online/kshatrainfotech/abol-app/public/storage/images/theaters/default.png",
"is_notify": false
}
]
}
here is how I use it for calling API
apimanager.fetch { [weak self] (response: Result<ResponseBaseModel<[Theater]>, ApiError>) in
self?.handelResponse(response: response) { response in
switch response {
case .success(let theaterList):
self?.theaterViewModels = theaterList.map{ TheaterViewModel(theaterModel: $0)}
self?.responseHandler(.success(self!.theaterViewModels))
case .failure(let apiError):
self?.responseHandler(.failure(apiError))
}
}
}
but some API doesn't have a result such as
{
"status": true,
"statusCode": 200,
"message": "api.DATA_UPDATED_SUCCESS"
}
how to handle the above response with the ResponseBaseModel model.because I have to pass any type while working with ResponseBaseModel struct.
ResponseBaseModel<?>
I already try ResponseBaseModel<nil> but it's not worked.
I try to simplyfy your question a little, to show why the thing you want is not possible this way. Think of the follwing structure:
struct TestStruct<T> {
let code:Int
var value:T?
init(code:Int) {
self.code = code
}
}
Typically you would use it this way:
var s = TestStruct<String>(code:42)
s.value = "abc"
print (s) // TestStruct<String>(code: 42, value: Optional("abc"))
Now you say: "Oh, I need a TestStruct only for some code, with no value at all" and try the following:
var codeOnly = TestStruct<nil>(code:42)
print (codeOnly)
The compiler complains, because it needs a dedicated type for the value property.
Why doesn't it work? Because think of the following statement:
let v = codeOnly.value
// or
codeOnly.value = "String"
Here, the compiler needs to know what type the property codeOnly.value is.
Since it cannot now, it will not allow a untyped TestStruct.

How to make Swift Codable types more versatile

I currently am working with an API that deals with bus predictions. There is an interesting quirk with the JSON that is returned for the predictions of a certain stop. When there are multiple predictions for a stop, the JSON looks something like this:
...
"direction": {
"prediction": [
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571787798536",
"isDeparture": "false",
"minutes": "50",
"seconds": "3008",
"tripTag": "122",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571789598536",
"isDeparture": "false",
"minutes": "80",
"seconds": "4808",
"tripTag": "123",
"vehicle": "1698"
}
],
"title": "Loop"
}
...
However, when there is only one prediction for a stop, the JSON looks like this instead:
...
"direction": {
"prediction":
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
}
"title": "Loop"
}
...
Notice that the "prediction" is no longer inside an array -- this is where I believe things are getting complicated when using a Swift Codable type to decode the JSON. My model looks like this for the "direction" and "prediction"
struct BTDirection: Codable {
let title: String!
let stopTitle: String!
let prediction: [BTPrediction]!
}
struct BTPrediction: Codable {
let minutes: String!
let vehicle: String!
}
Basically what is happening is prediction in BTDirection is looking for an Array of BTPrediction, however in the 2nd case above, this wouldn't be an Array so the decoding fails. How can I make my models more flexible to accommodate both an array or a single object? Ideally, in the 2nd case prediction would still be an array of a single BTDirection. Any help on this would be much appreciated.
You can try
struct BTDirection:Codable {
let title,stopTitle: String
let prediction: [BTPrediction]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decode(String.self, forKey: .stopTitle)
do {
let res = try container.decode([BTPrediction].self, forKey: .prediction)
prediction = res
}
catch {
let res = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [res]
}
}
}
To add on to Sh_Khan's answer, if you have multiple places in your API responses where this sort of thing happens, you can extract this custom decoding and encoding out to a custom wrapper type so you don't have to repeat it everywhere, like:
/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
private var elements: [Element]
}
extension ArrayOrSingleItem: Decodable where Element: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// First try decoding the single value as an array of `Element`s.
elements = try container.decode([Element].self)
} catch {
// If decoding as an array of `Element`s didn't work, try decoding
// the single value as a single `Element`, and store it in an array.
elements = try [container.decode(Element.self)]
}
}
}
extension ArrayOrSingleItem: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if elements.count == 1, let element = elements.first {
// If the wrapped array of `Element`s has exactly one `Element`
// in it, encode this as just that one `Element`.
try container.encode(element)
} else {
// Otherwise, encode the wrapped array just as it is - an array
// of `Element`s.
try container.encode(elements)
}
}
}
// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
// let directions: ArrayOrSingleItem<BTDirection> = ...
// let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
subscript(position: Int) -> Element {
get { elements[position] }
set { elements[position] = newValue }
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
func index(after i: Int) -> Int {
elements.index(after: i)
}
}
// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.elements = elements
}
}
Then you can just declare your prediction (and any other property that has the potential to be either an array or a single item in your API response) like this:
struct BTDirection: Codable {
let title: String?
let stopTitle: String?
let prediction: ArrayOrSingleItem<BTPrediction>?
}
Both #TylerTheCompiler & #Sh_Khan provide very good technical input into solution that provide the mechanics of a solution, but the provided code will hit some implementation issues with the given json data:
there are errors in the JSON posted that will stop codable working with it - I suspect these are just copy & paste errors, but if not you will have issues moving forwards.
Because of the initial direction key the JSON effectively has 3 (or at least 2.5!) layers of nesting. This will either need to be flattened in init(from:) or, as in the below, need a temporary struct for ease of mapping. Flattening in the initialiser would be more elegant, a temporary struct is far quicker :-)
CodingKeys, while obvious, isn't defined in the previous answers, so will cause errors compiling the init(from:)
there's no stopTitle field in the JSON, so that will error on decoding unless it is treated as an optional. Here I've treated it as a concrete String and handled it in the decoding; you could just make it a String? and then the decoder would cope with it being absent.
Using "corrected" JSON (added opening braces, missing commas, etc) the following code will import both scenarios. I've not implemented the arrayOrSingleItem as all credit for that belongs with #TylerTheCompiler, but you could easily drop it in.
struct Direction: Decodable {
let direction: BTDirection
}
struct BTDirection: Decodable {
enum CodingKeys: String, CodingKey {
case title
case stopTitle
case prediction
}
let prediction: [BTPrediction]
let title: String
let stopTitle: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
prediction = try container.decode([BTPrediction].self, forKey: .prediction)
} catch {
let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [singlePrediction]
}
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
}
}
struct BTPrediction: Decodable {
let minutes: String
let vehicle: String
}
and then to actually decode the JSON decode the top-level Direction type
let data = json.data(using: .utf8)
if let data = data {
do {
let bus = try decoder.decode(Direction.self, from: data)
// extract the BTDirection type from the temporary Direction type
// and do something with the decoded data
}catch {
//handle error
}
}
In case you're not aware, JSON Validator is very useful for validating/correcting json.

JSONDecoder failed to decode nested dictionaries

I'm using JSONDecoder to decode from a JSON file which has nested dictionaries. It fails to decode from the json data to my customized model.
This is what I have tried in my code.
The JSONDecoder looks like this:
let netWorkManager = NetWorkManager(URL: url, httpMethodType: .GET)
netWorkManager.callAPI { (data, status, error) in
guard let data = data else {
onFail(NetWorkError.otherError)
return
}
switch status {
case 200:
do{
if let responseModel = try JSONDecoder().decode(ResonseModel?.self, from: data) {
onSuccess(responseModel)
}
}catch {
onFail(NetWorkError.otherError)
}
default:
onFail(NetWorkError.otherError)
}
}
The model looks like this:
struct ResonseModel: Codable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Codable {
let version: String
let id: String
let key: Int
let name: String
let title: String
let blurb: String
}
}
The JSON structure looks like this:
{
"type": "champion",
"format": "standAloneComplex",
"version": "9.3.1",
"data": {
"Aatrox": {
"version": "9.3.1",
"id": "Aatrox",
"key": "266",
"name": "Aatrox",
"title": "the Darkin Blade",
"blurb": "Once honored defenders of Shurima against the Void, Aatrox and his brethren would eventually become an even greater threat to Runeterra, and were defeated only by cunning mortal sorcery. But after centuries of imprisonment, Aatrox was the first to find...",
"info": {
"attack": 8,
"defense": 4,
"magic": 3,
"difficulty": 4
},
"tags": [
"Fighter",
"Tank"
],
"partype": "Blood Well",
},
"Ahri": {
"version": "9.3.1",
"id": "Ahri",
"key": "103",
"name": "Ahri",
"title": "the Nine-Tailed Fox",
"blurb": "Innately connected to the latent power of Runeterra, Ahri is a vastaya who can reshape magic into orbs of raw energy. She revels in toying with her prey by manipulating their emotions before devouring their life essence. Despite her predatory nature...",
"info": {
"attack": 3,
"defense": 4,
"magic": 8,
"difficulty": 5
},
"tags": [
"Mage",
"Assassin"
],
"partype": "Mana",
},
...
this is the link for the JSON if you want to look into it: http://ddragon.leagueoflegends.com/cdn/9.3.1/data/en_US/champion.json
I want to decode the "data" property as a dictionary whose key is the name of the champion and value is the champion. But the jsonDecoder seems doesn't recognize my model structure. It ends up catch the error.
The JSON-parameter "key" is not an Integer.
Change it to String and it will work:
struct ResonseModel: Codable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Codable {
let version: String
let id: String
let key: String
let name: String
let title: String
let blurb: String
}
}
You can switch to manually decoding Champion in order to clean up the data.
struct ResonseModel: Decodable {
let type : String
let format: String
let data: [String: Champion]
struct Champion: Decodable {
let version: String
let id: String
let key: Int
let name: String
let title: String
let blurb: String
enum CodingKeys: String, CodingKey {
case version, id, key, name, title, blurb
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.id = try container.decode(String.self, forKey: .id)
guard let key = Int(try container.decode(String.self, forKey: .key)) else {
throw DecodingError.valueNotFound(Int.self,
.init(codingPath: decoder.codingPath,
debugDescription: "Bad value for id"))
}
self.key = key
self.name = try container.decode(String.self, forKey: .name)
self.title = try container.decode(String.self, forKey: .title)
self.blurb = try container.decode(String.self, forKey: .blurb)
}
}
}
This is basically the code that the compiler writes for you; it just converts the string into an int because that's what you really wanted.

Resources