How to add Dynamic key name and value in Encodable(Swift)? - swift

I have a very simple request:
{"token": "abcd", "key": "value" }
I'm trying to add this request as an Encodable. Now, here the issue arises that the key name can be anything like "123", "311", the type of the key will be String, but it's name is dynamic. How can I add dynamic names in Encodable?
struct Answers: Encodable {
let token: String
let key: String
}
I tried using generics, but it didn't work. Any one have any idea?

I don't think Codable allows that kind of functionality currently. You can't create a Codable type with dynamic keys as of now.
Alternatively, if this is the model you're using, you can simply create a Dictionary from it and then encode it using JSONEncoder().
Example:
let dict = ["token": "abcd", "1234": "value"]
do {
let response = try JSONEncoder().encode(dict)
print(response)
} catch {
print(error)
}

Rob,
If you could change the JSON response you can use something like that {token: "AAA", data: {"key":"123"}}.
So you can create
struct Response<DataType: Codable>: Codable {
let token: String
let data: DataType
}
With this Struct, you can pass many combinations of dynamic values.
My solution doesn't work with your actual data, but maybe you can talk with the team about API and maybe change the data.

Related

Transforming Alamofire response when using Codable and Combine

I want to use Alamofire to query my backend, encode the response using Alamofire's built-in Codable parsing and then publish an extract from the resulting Struct to be consumed by the caller of my API class. Say I have some JSON data from my backend (simplified, but shows the structure):
{
"total": 123,
"results": [
{"foo" : "bar"},
{"foo" : "baz"}
]
}
and the associated Codable Structs
struct MyServerData: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let foo: String
}
I can get, parse, publish, and subscribe all fine with the following:
func myAPI() -> DataResponsePublisher<MyServerData> {
return AF.request("https://backend/path")
.validate()
.publishDecodable(type: MyServerData.self)
}
myAPI()
.sink { response in /* Do stuff, e.g. update #State */ }
What I'd like to do is to publish just the [Result] array. What's the correct approach to this? Should I use .responseDecodable() and create a new publisher (somehow - .map()?) that returns a [Result].publisher?
While I think I understand the reactive/stream based principles my Combine-fu is still weak and I don't have a clear handle on the transformation of one publisher into another (I'm guessing?)
Thanks in advance!
In addition to using Combine API like map, Alamofire offers two publishers on DataResponsePublisher itself.
.result() extracts the Result from the DataResponse and creates an AnyPublisher<Result<Value, AFError>, Never>.
.value() extracts the Value from the DataResponse and creates a failable publisher, AnyPublisher<Value, AFError>.
So depending on what kind of error handling you want, this could be as simple as:
...
.publishDecodable(...)
.value()
.map(\.results)

enum encoded value is nil while storing the class object in UserDefaults. Codable Protocol is already inherited

I am new to iOS and trying to store User object in UserDefaults. So that when the app is launched again, I can check user type and based on it, I need to navigate to relevant screen.
For that, I have created a User class as below (Codable) and it has one userType enum property!
enum UserType: Int, Codable {
case userType1 = 0
case userType2 = 1
case notDetermined = 2
init(from decoder: Decoder) throws {
let label = try decoder.singleValueContainer().decode(Int.self)
self = UserType(rawValue: label) ?? .notDetermined
}
}
class User: Codable {
public var userFullName: String? = ""
public var userType: UserType? //= .notDetermined
enum CodingKeys: String, CodingKey {
case userFullName
}
}
In my view Controller class, I am creating a new instance for User object and trying to store in user defaults as below:
let newUser = User()
newUser.userFullName = "Test"
newUser.userType = userTypeBtn.isSelected ? .userType1 : .userType2
when I print the newUser's userType, I can see proper value whichever is selected. But after that, when I am trying to store it in userDefaults as below, it returns nil for userType property.
do {
let encoded = try JSONEncoder().encode(newValue)
UserDefaults.standard.set(encoded, forKey: UserDefaultKey.currentUser)
UserDefaults.standard.sync()
} catch {
print("Unable to Encode User Object: (\(error))")
}
when I tried to print this encoded variable, and decoded it in console
JSONDecoder().decode(User.self, from: encoded).userType
it prints nil.
Please help me how can I store optional enum property in UserDefaults and retrieve it when needed using Codable
You should include userType in your CodingKeys enum:
enum CodingKeys: String, CodingKey {
case userFullName
case userType
}
Or just delete the CodingKeys enum entirely, since by default, all the properties are included as coding keys. The keys in the CodingKeys enum determines what the synthesised Codable implementation will encode and decode. If you don't include userType, userType will not be encoded, so it will not be stored into UserDefaults.
I am not getting it from Server and userType is an external property outside the JSON response
This is fine, because userType is optional. If the JSON does not have the key, it will be assigned nil. This might be a problem if you are also encoding User and sending it to the server, and that the server can't handle extra keys in the request, in which case you need two structs - one for storing to/loading from UserDefaults, one for parsing/encoding server response/request.
Remember to encode a new User to UserDefaults when you try this out, since the old one still doesn't have the userType encoded with it.
Observations
Having a custom implementation for Decodable part of enum UserType: Int, Codable is probably not the best idea. Swift compiler supports encoding/decoding enum X: Int out of the box without having you to write custom implementation for it. (In fact, starting with Swift 5.5, Swift compiler can now do this for enums that have cases with associated values as well.)
You should try to avoid having cases like .notDetermined. Either user has a type that's well defined or user.type is nil. You can easily define convenience getters on user itself to know about it's type.
Swift allows nesting of types, so having User.Kind instead of UserType is more natural in Swift.
Following implementation takes care of all of these points.
import Foundation
class User: Codable {
enum Kind: Int, Codable {
case free = 1
case pro = 2
}
public var fullName: String?
public var kind: Kind?
}
let newUser = User()
newUser.fullName = "Test"
newUser.kind = .free
do {
let encoded = try JSONEncoder().encode(newUser)
UserDefaults.standard.set(encoded, forKey: "appUser")
if let fetched = UserDefaults.standard.value(forKey: "appUser") as? Data {
let decoded = try JSONDecoder().decode(User.self, from: fetched)
print(decoded)
}
}
Above code includes definition, construction, encodeAndStore, fetchAndDecode and it does everything you need without any custom implementation.
Bonus
Above code does not print a nice description for the User. For that, you can add CustomStringConvertible conformance like this.
extension User: CustomStringConvertible {
var description: String {
"""
fullName: \(fullName ?? "")
kind: \(kind?.description ?? "")
"""
}
}
extension User.Kind: CustomStringConvertible {
var description: String {
switch self {
case .free: return "free"
case .pro: return "pro"
}
}
}
If you try print(decoded) after implementing this, you will clearly see what you want to see for User instance.
User.kind can be nil and I don't want to handle it with if let every time I need to check this from different screens in the app.
No worries, it can be simplified to this.
extension User {
var isFreeUser: Bool { kind == .free }
var isProUser: Bool { kind == .pro }
}

Fields in custom data model are "nil" after decoding API response with JSONDecoder().decode in Swift 5

I've defined a custom data model for a User Object in Swift like so:
user data model
I've got a function that pulls User data from an API like so:
get data from api
Here's the response when calling the same endpoint with Postman:
api response
And here's the console debug output from line 75 of my function, showing that I'm actually receiving that data: debug output
So that all looks good as far as I can tell.
I'm then using JSONDecoder().decode to decode the jsonData I receive from the api, for which I'm not getting any errors. However, when I'm then printing a field from the returned user object, that field (as well as all others) are "nil": all fields in user object are nil
I'm sure it's something small and stupid but I've spent hours now and can't figure out what it is. Can anyone spot the error and let me know what I'm doing wrong here?
Help much appreciated!!!
For Codable you need to give same name of properties to the json key. And make sure it's in correct scope. For example you email properties inside of detailresponse json object & detailresponse inside of main json object. If you don't wont more class you need to use it's init container method.
class Response: Codable {
var statuscode: Int?
var response_type: Int?
// Other properties
var detailresponse: DetailResponse?
}
class DetailResponse: Codable {
var id: Int?
// Other properties
var socialmediadata: User?
}
class User: Codable {
var id: Int?
var email: String?
// Other properties
}
Now, json will parse like this.
let response = try JSONDecoder().decode(Response.self, from: jsonData)
print(response.detailresponse?.socialmediadata?.email ?? "")

Alamofire POST request replacing characters in output

I am making this request:
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
print("Result: \(response.result.value)"
do {
self.list = try JSONDecoder().decode([list].self, from: result!) for event in self.lists {
print(event.title," : ",event.description)
}
} catch let parseError as NSError {
print("JSON Error \(parseError.localizedDescription)")
}
}
Data that ought to look like this (JSON?) - Postman output, all fields not included herein:
{
"start": "2016-02-01 11:30:00",
"end": "2016-02-01 14:42:24",
"id": 3192,
"ownership": false,
}
prints out looking like this in XCode:
{
start = "2016-02-01 11:30:00";
end = "2016-04-14 20:30:00";
"id" = 3192;
ownership = 0;
}
Result : I am not able to parse this using JSONDecoder, error:
"The data couldn’t be read because it isn’t in the correct format".
Newbie to Swift ... so, thanks in advance for the help!
Edit: Edited for clarity with more information. Thanks again!
Alamofire is not "replacing characters in output", it is giving you a different object than the one you expect. If you print out the type of your response.result you should be surprised by the NSDictionary you are likely to get at that point. Our trusted friend print(...) is nice enough to turn this into a String representation of whatever you pass it, but you are not likely to be able to parse this using JSONDecoder since it is not Data (which is what the decoder is expecting).
As I said before: use responseString in order to get the response and turn it into the appropriate Data for parsing using JSONDecoder. In order to be able to control this process properly you want to include your Codable derivative into the question and you are likely to set the date parsing strategy on the JSONDecoder.
Without your struct and some properly formatted JSON from your response (well, Postman will do if it is reasonably complete) we are unlikely to be able to help you any further.
P.S.: It is not an entirely good idea to change your question completely through an edit. You might be better of posting a new question and leaving a comment with a pointer to it on the old one so people revisiting it may be lead to the right place. If you update your question you should usually leave the old one intact and amend it with additional information in order to keep the existing discussion relevant.
As workaround you can just add CodingKey to decoded struct.
Just add to your struct/class
private enum CodingKeys: String, CodingKey {
case event_id = "id"
}
Please refer to https://benscheirman.com/2017/06/swift-json/
I can suggest the following solution:
Firstly you need a pojo class to refer your json object. Easiest way
that I know is the library called SwiftyJSON
(https://github.com/SwiftyJSON/SwiftyJSON) firstly you can add this
library to your project. Then you can create the following pojo class
for your output (optional: You can also install
SwiftyJSONAccelarator(https://github.com/insanoid/SwiftyJSONAccelerator)
to generate pojo classes using json outputs.):
import Foundation
import SwiftyJSON
public class MyOutput: NSObject {
// MARK: Declaration for string constants to be used to decode and also serialize.
internal let kMyOutputEndKey: String = "end"
internal let kMyOutputInternalIdentifierKey: String = "id"
internal let kMyOutputOwnershipKey: String = "ownership"
internal let kMyOutputStartKey: String = "start"
// MARK: Properties
public var end: String?
public var internalIdentifier: Int?
public var ownership: Bool = false
public var start: String?
// MARK: SwiftyJSON Initalizers
/**
Initates the class based on the object
- parameter object: The object of either Dictionary or Array kind that was passed.
- returns: An initalized instance of the class.
*/
convenience public init(object: AnyObject) {
self.init(json: JSON(object))
}
/**
Initates the class based on the JSON that was passed.
- parameter json: JSON object from SwiftyJSON.
- returns: An initalized instance of the class.
*/
public init(json: JSON) {
end = json[kMyOutputEndKey].string
internalIdentifier = json[kMyOutputInternalIdentifierKey].int
ownership = json[kMyOutputOwnershipKey].boolValue
start = json[kMyOutputStartKey].string
}
}
After that after calling url with Alomofire and getting response, you
can simply map the output to your pojo class. Finally, you can use any
field in your class(myOutput in my example):
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
let myOutput = MyOutput.init(json: json)
//use myOutput class for your needs
case .failure( _):
self.createNetworkErrorPopup()
}
}

Using Swift 4 Codable Protocol with Unknown Dictionary Keys

I am working with NASA's Near Earth Object Web Service to retrieve data to be displayed in an application. I understand how to use Swift 4's Codable protocol, but I do not understand how to map part of the response.
Using Paw, I inspected the response from the API:
As you can see, the near_earth_objects structure is a Dictionary, and the keys are dates. The issue is that the URL parameters are dates, so these date structures will change, depending on the day of the request. Therefore, I do not know how I can create properties to be automatically mapped when using the Codable protocol.
The data that I am trying to get to inside of these structures are Arrays that contain Dictionarys:
How can I have my model object conform to the Codable protocol and map these structures when the dates will change as the dates of the requests change?
You don't need to know the keys of the Dictionary compile time if you don't mind keeping a Dictionary after decoding.
You just need to specify the property with type Dictionary<String:YourCustomDecodableType>. The keys will be dates corresponding to observation and the value will an array of all objects with your custom type.
struct NearEarthObject: Codable {
let referenceID:String
let name:String
let imageURL:URL
private enum CodingKeys: String, CodingKey {
case referenceID = "neo_reference_id"
case name
case imageURL = "nasa_jpl_url"
}
}
struct NEOApiResponse: Codable {
let nearEarthObjects: [String:[NearEarthObject]]
private enum CodingKeys: String,CodingKey {
case nearEarthObjects = "near_earth_objects"
}
}
do {
let decodedResponse = try JSONDecoder().decode(NEOApiResponse.self, from: data)
} catch {
print(error)
}
As you said, near_earth_objects is a Dictionary, but keys are not Dates, keys are Strings, and values are arrays of the known structures. So the above code will work:
...
let nearEarthObjects: [String: [IndexObject]]
...
enum CodingKey: String, CodingKeys {
case nearEarthObjects = "near_earth_objects"
}
struct IndexObject: Decodable {
...
let name: String
...
}