Dynamically change API data fetching structure - ios

I am fetching data from an API that keeps changing it's format several times a day from String to Double erratically..
Is it possible to do something to the struct to prevent returning nil when fetching and automatically using with the right type?
struct BitcoinGBP : Decodable {
let price : Double
let percentChange24h : Double
private enum CodingKeys : String, CodingKey {
case price = "PRICE"
case percentChange24h = "CHANGEPCT24HOUR"
}
}
Would simply using Double? work?

Write a custom initializer to handle both cases
struct BitcoinGBP : Decodable {
let price : Double
let percentChange24h : Double
private enum CodingKeys : String, CodingKey {
case price = "PRICE"
case percentChange24h = "CHANGEPCT24HOUR"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
percentChange24h = try container.decode(Double.self, forKey: .percentChange24h)
do {
price = try container.decode(Double.self, forKey: .price)
} catch DecodingError.typeMismatch(_, _) {
let stringValue = try container.decode(String.self, forKey: .price)
price = Double(stringValue)!
}
}
}

In my experience you need to design your structures, models to correspond to what you expect to use in your application and not what API will give you. You need to be prepared for the API to give you incorrect data types or missing, strange data.
If API changes too much then users will need to update but this is generally never done. Endpoints must have versions and need to change version when a big difference is made. If this is not the case that it is flawed API (these things still do happen) in which case you should use your own server, proxy which connects to desired server and modifies their API to your own. You put some tests on it to detect if their API has changed and modify your own as quickly as possible so all applications can still connect to it. But this is way out of the scope of this question.
If your structure must be with having price and percentage change in 24h where both are double then simply expect string or anything from the API. I like to avoid any tools that do that automatically so I just use convenience constructors and a set of tools of my own to produce something like:
class BitcoinGBP {
var price: Double = 0.0
var priceChange24h: Double = 0.0
convenience init(price: Double, priceChange24h: Double) {
self.init()
self.price = price
self.priceChange24h = priceChange24h
}
/// Construuctor designed to accept JSON formated dictionary
///
/// - Parameter descriptor: A JSON descriptor
convenience init(descriptor: [String: Any]) {
self.init()
self.price = double(value: descriptor["PRICE"]) ?? 0.0
self.priceChange24h = double(value: descriptor["CHANGEPCT24HOUR"]) ?? 0.0
}
/// A conveninece method to extract a double value
///
/// - Parameter value: A value to be converted
/// - Returns: Double value if possible
func double(value: Any?) -> Double? {
if let value = value as? String {
return Double(value)
} else if let value = value as? Double {
return value
} else if let value = value as? Float {
return Double(value)
} else if let value = value as? Int {
return Double(value)
} else if let value = value as? Bool {
return value ? 1.0 : 0.0
} else {
return nil
}
}
}
Now the rest is just depending on what functionality you need.

Related

working with similar json objects, casting int to string [duplicate]

This question already has answers here:
Using codable with value that is sometimes an Int and other times a String
(5 answers)
Closed last year.
i am working with a service that have both a websocket for live data and a api for historical data
the JSON looks similar and i would like to decode it to the same object
the only difference is that in the live one variable is a number but as a string and with the historical data the number is an int.
and preferably i would like to not have to create 2 almost identical decodable objects.
have anyone tried something similar.
You have to define a single type (Int or String) for your data structure and use init with Decoder to make a custom parsing.
struct MyData: Decodable {
let value: Int // Could be Int or String from different services
}
extension MyData {
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode(Int.self, forKey: .value)
} catch {
let stringValue = try container.decode(String.self, forKey: .value)
if let valueInt = Int(stringValue) {
value = valueInt
} else {
var codingPath = container.codingPath
codingPath.append(CodingKeys.value)
let debugDescription = "Could not create Int from String \(stringValue) of field \(CodingKeys.value.rawValue)"
let context = DecodingError.Context(codingPath: codingPath, debugDescription: debugDescription)
throw DecodingError.dataCorrupted(context)
}
}
}
}
I think you need a wrapper for that case of some sort. To make it as convenient as possible you could use a property wrapper for this
#propertyWrapper
struct NormalOrStringyInt: Decodable {
var wrappedValue: Int?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
wrappedValue = value
} else if
let string = try? container.decode(String.self),
let value = Int(string)
{
wrappedValue = value
} else {
wrappedValue = nil // default value
}
}
}
struct Model: Codable {
#NormalOrStringyInt var id: Int?
var someInt: Int
var someString: String
...
}
let model = try! JSONDecoder().decode(Model, from: data)
let id: Int? = model.id.wrappedValue

How to decode any json value to string with Decoadable object in Swift?

According my question, I want to decode every fields of my json to string value.
My json look like this
{ name: "admin_tester",
price: 99.89977202,
no: 981,
id: "nfs-998281998",
amount: 98181819911019.828289291329 }
And I want to create my struct like this
struct StockNFS: Decodable {
let name: String?
let price: String?
let no: String?
let id: String?
let amount: String?
}
But If I declare my struct like this, When I use json decode I will get error mismatch type
The reason why I want to mapping every value to string, It is because If I use a double or decimal for price and amount, after encode sometime value will incorrect. example 0.125, I wil got 0.124999999.
I just want to recieve any data in string type for just showing on ui ( not edit or manipulate value )
I will appreciate any help. Thank you so much.
To avoid the floating point issues we can either use a type String or Decimal for the keys price and amount. In either case we can not decode directly into either type but we first need to use the given type which is Double so we need a custom init for this.
First case is to use String (I see no reason to use optional fields as default, change this if any of the fields actually can be nil)
struct StockNFS: Codable {
let name: String
let price: String
let no: Int
let id: String
let amount: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
let priceValue = try container.decode(Double.self, forKey: .price)
price = "\(priceValue.roundToDecimal(8))"
//... rest of the values
}
}
The rounding is used with a method inspired from this excellent answer
extension Double {
func roundToDecimal(_ fractionDigits: Int) -> Double {
let multiplier = pow(10, Double(fractionDigits))
return (self * multiplier).rounded() / multiplier
}
}
To do the same but with the numeric type Decimal we do
struct StockNFS2: Codable {
let name: String
let price: Decimal
let no: Int
let id: String
let amount: Decimal
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
let priceValue = try container.decode(Double.self, forKey: .price)
price = Decimal(priceValue).round(.plain, precision: 8)
//... rest of the values
}
}
Again the rounding method was inspired from the same answer
extension Decimal {
func round(_ mode: Decimal.RoundingMode, precision: Int = 2) -> Decimal {
var result = Decimal()
var value = self
NSDecimalRound(&result, &value, precision, mode)
return result
}
}

How do I properly decode this json string using decodable?

I have the following json string:
{"weight":[{"bmi":24.75,"date":"2020-01-20","logId":1000,"source":"API","time":"23:59:59","weight":200}]}
I want to convert it to a Swift object in order to access the different values. Here is what I am trying to do, I have these structs setup:
struct FitbitResponseModel: Decodable {
let weight: [FitbitResponseData]
}
struct FitbitResponseData: Decodable {
let bmi: Int
let date: String
let logId: Int
let source: String
let time: String
let weight: Int
}
And then I have this method to decode the json string:
func parseJSON(data: Data) -> FitbitResponseModel? {
var returnValue: FitbitResponseModel?
do {
returnValue = try JSONDecoder().decode(FitbitResponseModel.self, from: data)
} catch {
print("Error took place: \(error.localizedDescription).")
}
return returnValue
}
However when I try to run it I get the error that the data couldn’t be read because it isn’t in the correct format. What am I doing wrong? Any help is appreciated.
Thanks in advance!
change
let bmi: Int
to
let bmi: Double
beacuse it's value is coming out to be 24.75 in your response if any variable type doesn't match to JSON response whole model wouldn't map in Codable protocol (Encodable and Decodable)
Talk to your API developer. 000 is not a valid representation of a number for json. It needs to be either 0 or 0.0. You can lint your json at https://jsonlint.com . If you really need to work around this I suggest doing a string replacement on 000, with 0, before you parse the data.
Json is n't valid because logId value in your json is n't valid.
{
"weight": [{
"bmi": 24.75,
"date": "2020-01-20",
"logId": 100,
"source": "API",
"time": "23:59:59",
"weight": 200
}]
}
One really neat feature of this auto-generated conformance is that if you define an enum in your type called "CodingKeys" (or use a type alias with this name) that conforms to the CodingKey protocol – Swift will automatically use this as the key type. This therefore allows you to easily customise the keys that your properties are encoded/decoded with.
struct Base: Codable {
let weight : [Weight]?
enum CodingKeys: String, CodingKey {
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
weight = try values.decodeIfPresent([Weight].self, forKey: .weight)
}
}
struct Weight : Codable {
let bmi : Double?
let date : String?
let logId : Int?
let source : String?
let time : String?
let weight : Int?
enum CodingKeys: String, CodingKey {
case bmi = "bmi"
case date = "date"
case logId = "logId"
case source = "source"
case time = "time"
case weight = "weight"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
bmi = try values.decodeIfPresent(Double.self, forKey: .bmi)
date = try values.decodeIfPresent(String.self, forKey: .date)
logId = try values.decodeIfPresent(Int.self, forKey: .logId)
source = try values.decodeIfPresent(String.self, forKey: .source)
time = try values.decodeIfPresent(String.self, forKey: .time)
weight = try values.decodeIfPresent(Int.self, forKey: .weight)
}
}
Hope that will help!
or you can use SwiftyJSON lib: https://github.com/SwiftyJSON/SwiftyJSON

swift : shortcut for guard "let self = ..."?

I have to parse json server response into a swift object. I use this code :
struct MyGPSCoords {
var latitude:Double
var longitude:Double
var accuracy:Int
var datetime:NSDate
init?(infobrutFromJson_:[String:String]?)
{
guard let infobrut = infobrutFromJson_ else {
// first time the user sign up to the app, php server returns "null" in Json
return nil
}
guard
let lat:Double = Double(infobrut["latitude"] ?? "nil"),
let lng = Double(infobrut["longitude"] ?? "nil"),
let acc = Int(infobrut["accuracy"] ?? "nil"),
let dtm = NSDate(timeIntervalSince1970: Double(infobrut["time"] ?? "nil"))
else {
print("warning : unable to parse data from server. Returning nil");
return nil ; // position not NIL but format not expected => = nil
}
self.latitude = lat
self.longitude = lng
self.accuracy = acc
self.datetime = dtm
}
}
I want to make the "guard" statement as short as possible. First, I added ?? "nil" so if one of the keys doesn't exist, Double("nil") gets nil and guard statement can handle. For NSDate, I made an extension with a convenience init? returning nil if its input is nil, so I can do the same.
My question is, can i do it even shorter by assigning directly to self.latitude the values right in the guard statement ? I tried that :
guard self.latitude = Double(infobrut["latitude"] ?? "nil"), ...
It says it cannot cast from Double? to Double. Is there any way to make this guard even shorter and avoiding me to assign lat, lng, acc and dtm buffering variables ?
First, you should of course try to fix the JSON, since this JSON is malformed. Strings are not numbers in JSON. Assuming you cannot correct this broken JSON, the tool you want is flatMap, which converts T?? to T? (which is what guard-let expects).
guard
let lat = infobrut["latitude"].flatMap(Double.init),
let lng = infobrut["longitude"].flatMap(Double.init),
let acc = infobrut["accuracy"].flatMap(Int.init),
let dtm = infobrut["time"].flatMap(TimeInterval.init).flatMap(Date.init(timeIntervalSince1970:))
else {
print("warning : unable to parse data from server. Returning nil")
return nil // position not NIL but format not expected => = nil
}
I saw a lot of comments that Codable won't work here, but it absolutely will, and it's really what you should use. Here's one way (this is a little sloppy about its error messages, but it's simple):
struct MyGPSCoords: Decodable {
var latitude:Double
var longitude:Double
var accuracy:Int
var datetime:Date
enum CodingKeys: String, CodingKey {
case latitude, longitude, accuracy, datetime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard
let lat = Double(try container.decode(String.self, forKey: .latitude)),
let lng = Double(try container.decode(String.self, forKey: .longitude)),
let acc = Int(try container.decode(String.self, forKey: .accuracy)),
let dtm = TimeInterval(try container.decode(String.self,
forKey: .datetime)).flatMap(Date.init(timeIntervalSince1970:))
else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Could not decode"))
}
self.latitude = lat
self.longitude = lng
self.accuracy = acc
self.datetime = dtm
}
}
Or you can get really fancy with an internal helpful function and get rid of all the temporary variables and optionals through the power of throws.
struct MyGPSCoords: Decodable {
var latitude:Double
var longitude:Double
var accuracy:Int
var datetime:Date
enum CodingKeys: String, CodingKey {
case latitude, longitude, accuracy, datetime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
func decodeBrokenJSON<T>(_ type: T.Type,
forKey key: CodingKeys) throws -> T
where T: Decodable & LosslessStringConvertible {
return try T.init(container.decode(String.self, forKey: key)) ?? {
throw DecodingError.dataCorruptedError(forKey: key,
in: container,
debugDescription: "Could not decode \(key)")
}()
}
self.latitude = try decodeBrokenJSON(Double.self, forKey: .latitude)
self.longitude = try decodeBrokenJSON(Double.self, forKey: .longitude)
self.accuracy = try decodeBrokenJSON(Int.self, forKey: .accuracy)
self.datetime = Date(timeIntervalSince1970: try decodeBrokenJSON(TimeInterval.self, forKey: .datetime))
}
}
(IMO, this is a great example of how throws really shines and should be used much more than it commonly is.)
What you want to do is not possible. The compiler already tells you so, even though the error message is a bit misleading. You can either use a guard let that creates a new variable, or you can use a guard with a boolean expression. In your case there is no let so the compiler tries to parse a boolean expression. Instead it sees the assignment and produces the error message that the types don’t match. If the types would match (as in guard self.latitude = 12.0) the error message would be clearer: error: use of '=' in a boolean context, did you mean '=='?
The other solutions seem overly complicated. Simply make it
struct MyGPSCoords: Codable {
var latitude: Double?
var longitude: Double?
var accuracy: Int?
var datetime: Date?
var isValid {
return [latitude, longitude, accuracy, datetime].filter { $0 == nil }.isEmpty
}
}
// jsonData is whatever payload you get back from the URL request.
let coords = JSONDecoder().decode(jsonData, type: MyGPSCoords.self)
if !coords.isValid {
print("warning : unable to parse data from server.")
}
Since all of your properties are Optional, parsing can't fail if one or more of them is missing. The isValid check is much simpler than the guard let... clause in your original code.
EDIT: If, as Rob Napier suggests, all the JSON values are encoded as Strings, then here's another way to structure your MyGPSCoords:
struct MyGPSCoords: Codable {
// These are the Codable properties
fileprivate var latitudeString: String?
fileprivate var longitudeString: String?
fileprivate var accuracyString: String?
fileprivate var datetimeString: String?
// Default constant to use as a default check for validity
let invalid = Double.leastNonzeroMagnitude
// And these are the derived properties that you want users to use
var latitude: Double {
return Double(latitudeString ?? "\(invalid)") ?? invalid
}
var longitude: Double {
return Double(longitudeString ?? "\(invalid)") ?? invalid
}
var accuracy: Int {
return Int(accuracyString ?? "\(invalid)") ?? Int(invalid)
}
var date: Date {
return <whatever-formatter-output-you-need>
}
var isValid {
return [latitudeString, longitudeString, accuracyString, datetimeString].filter { $0 == nil }.isEmpty
&& latitude != invalid && longitude != invalid
&& accuracy != Int(invalid) /* && however you compare dates */
}
}
I know that question is old but I have to admit that I didn't understand very well the in-built Decodable/Decoder system in Swift (especially this notion of "Container", can't figure out what it represents exactly)
Anyway, I made my own decoder which enable to handle this situation in quite the same way as Android does (to decode JSONObject). I made an extension of Dictionary like this :
protocol Decodable {
init(from raw:[String:Any]) throws
}
extension Dictionary where Key == String
{
enum DecodableError : Error {
case unknownKey(key:String)
case keyWrongType(key:String, expectedType:String, actualValue:String)
case nullValueAtKey(key:String)
}
func getSafe<T>(_ key:String, forType t:T.Type) throws -> T
{
if(self[key] != nil)
{
if(self[key] is NSNull) // corresponds to the JSON null value (by experience)
{
throw DecodableError.nullValueAtKey(key:key)
}
else if(self[key] is T) // for raw type
{
return self[key] as! T
}
// try to parse self[key] to provided type if it's decodable
else if(self[key] is [String:Any] && t is Decodable.Type)
{
return try (t as! Decodable.Type).init(from: self[key] as! [String:Any]) as! T
}
throw DecodableError.keyWrongType(key: key,
expectedType: String(describing: T.self),
actualValue: String(describing:self[key]!))
}
throw DecodableError.unknownKey(key:key)
}
func getSafeOpt<T>(_ key:String, forType t:T.Type) throws -> T?
{
if(self[key] != nil)
{
if(self[key] is NSNull)
{
return nil
}
return try getSafe(key, forType: t)
}
throw DecodableError.unknownKey(key:key)
}
}
I use it like that :
struct Position : Decodable {
let latitude:Double
let longitude:Double
let accuracy:Int?
let member:Member
init(from raw:[String:Any]) throws
{
// getSafe throw exception whenever node are json<null> or if node doesn't exist
latitude = try raw.getSafe("lat", forType: Double.self)
longitude = try raw.getSafe("lng", forType: Double.self)
// getSafeOpt returns nil if the JSON value of node acc is null,
// but it still throw an exception if there is no "acc" node
accuracy = try raw.getSafeOpt("acc", forType: Int.self)
// you can use it to decode other objects that implement my Decodable protocol too :
member = try raw.getSafeOpt("member", forType: Member.self)
}
}
do {
try app.position = Position(from: rawDic)
}
catch {
print("Unable to parse Position : \(error) ")
return
}
This does not handle yet the JSON arrays, I'll do it later, or feel free to update my answer if you wish to add a JSON array handling mechanism.

How to implement model class for multiple values in swift 3?

Here I am having value in JSON in which for some of multiple key value pairs it returning string and for some it is returning array here in custom attributes array in first dictionary in that value key value pair the data present is different and in the second dictionary value key value pair is different here then how to implement the model class for inside array for different key values ?
struct MediaGallery {
let id : Int
let mediaType : String
let label : Any
let position : Int
let disabled : Any
let file : String
init(dict : [String:Any]) {
self.id = (dict["id"] as? Int)!
self.mediaType = (dict["media_type"] as? String)!
self.label = dict["label"]!
self.position = (dict["position"] as? Int)!
self.disabled = dict["disabled"]!
self.file = (dict["file"] as? String)!
}
}
struct AttributeList {
let label : String
let value : String
let code : String
init(dict : [String:Any]){
self.label = (dict["label"])! as! String
self.value = (dict["value"])! as! String
self.code = (dict["code"])! as! String
}
}
struct DetailsListAttribute {
let attributeCode : String
let value : Any
init?(dict : [String:Any]) {
self.attributeCode = dict["attribute_code"] as! String
print(self.attributeCode)
if let values = dict["value"] as? String {
self.value = values
}
else {
if let arr = dict["value"] as? [[String:Any]]{
var filterArr = [AttributeList]()
for obj in arr {
filterArr.append(AttributeList(dict: obj))
}
self.value = filterArr
} else {
self.value = [AttributeList]()
}
}
}
}
I would suggest please save some time by using this great GIT Library ObjectMapper . it will help you to model your object and convert your model objects (classes and structs) to JSON and vice versa.
I've tried multiple JSON-mapping frameworks that were mentioned in Tj3n comment. They all have pros and cons. Apple suggests you to follow the recommendation given here. Also you should check Codable protocol (swift 4 is required).
Ok I don't have the whole JSON, and it doesn't seem clear to me.
But here is how you can parse and create your model Class easily in Swift with the Codable protocol.
You can read more about it and/or some examples, tutorials : Ultimate Guide.
Briefly, what is the Codable protocol ?
You don't need third party library anymore in order to parse and set the json data to your model class.
You juste have to create your class like the JSON is represented. And according to the key-name, it will create the class, properties and everything for you.
Here is an example with your JSON, I don't know if I understood your JSON formatting, but you got the trick :
struct Response: Codable {
let ca: [CustomAttribute]?
enum CodingKeys: String, CodingKey {
case ca = "custom_attributes"
}
}
struct CustomAttribute: Codable {
let code: String?
let value: [Value]?
struct Value: Codable {
let label: String?
let value: String?
let code: String?
let avg: String? // I don't know how your value array is composed
let count: Int? // I don't know how your value array is composed
}
enum CodingKeys: String, CodingKey {
case code = "attribute_code"
case avg = "avg_rating_percent"
}
}
For me, it looks like something like that.
I don't see the whole JSON, but imagine you have the whole JSON as the Response Struct, it contains several objects, like the CustomAttribute Array for example.
Then you can define the CustomAttribute structure, and add as many properties as the JSON has.
Anyway, you can call it this way :
When you have the response from your API call, you can go :
if let data = response.data {
let decoder = JSONDecoder()
let response = try! decoder.decode(Response.self, from: data)
print("Only printing the Custom Attribute : \(response.ca!)")
}
I decode the whole json data as an Object Response (like my Struct).
And I pass to my response callback, or
this might be late but I think this will helps others
The model class which are varies for frameworks like SwiftyJSON, simple swift class, Gloss or swift codable (Swift 4). you can easily generate model class online with your customization jsoncafe.com

Resources