Parsing json blob field in Swift - json

I am reading JSON from a web service in swift which is in the following format
[{
"id":1,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"5555555",
"logo":[-1,-40,-1,-32],
"shopPath":"test"
},
{
"id":2,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"66666666",
"logo":[-1,-50,-2,-2],
"shopPath":"test"
}]
I have managed to read all the strings easily but when it comes to the logo part I am not sure what should I do about it, this is a blob field in a mySQL database which represent an image that I want to retrieve in my swift UI, here is my code for doing that but i keep getting errors and not able to figure the right way to do it:
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: [String]
let path: String
}
func getBrandsJson() {
let url = URL(string: "http://10.211.55.4:8080/exam/Test")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else {
print(error!);
return
}
print(response.debugDescription)
let decoder = JSONDecoder()
let classes = try! decoder.decode([Brand].self, from: data)
for myClasses in classes {
print(myClasses.branch)
if let imageData:Data = myClasses.logo.data(using:String.Encoding.utf8){
let image = UIImage(data:imageData,scale:1.0)
var imageView : UIImageView!
}
}
}).resume()
}
Can someone explain how to do that the right way I have searched a lot but no luck

First of all, you should better tell the server side engineers of the web service, that using array of numbers is not efficient to return a binary data in JSON and that they should use Base-64 or something like that.
If they are stubborn enough to ignore your suggestion, you should better decode it as Data in your custom decoding initializer.
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: Data
let path: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: CodingKeys.id)
self.name = try container.decode(String.self, forKey: CodingKeys.name)
self.branch = try container.decode(String.self, forKey: CodingKeys.branch)
self.address = try container.decode(String.self, forKey: CodingKeys.address)
self.phone = try container.decode(String.self, forKey: CodingKeys.phone)
self.path = try container.decode(String.self, forKey: CodingKeys.path)
//Decode the JSON array of numbers as `[Int8]`
let bytes = try container.decode([Int8].self, forKey: CodingKeys.logo)
//Convert the result into `Data`
self.logo = Data(bytes: bytes.lazy.map{UInt8(bitPattern: $0)})
}
}
And you can write the data decoding part of your getBrandsJson() as:
let decoder = JSONDecoder()
do {
//You should never use `try!` when working with data returned by server
//Generally, you should not ignore errors or invalid inputs silently
let brands = try decoder.decode([Brand].self, from: data)
for brand in brands {
print(brand)
//Use brand.logo, which is a `Data`
if let image = UIImage(data: brand.logo, scale: 1.0) {
print(image)
//...
} else {
print("invalid binary data as an image")
}
}
} catch {
print(error)
}
I wrote some lines to decode array of numbers as Data by guess. So if you find my code not working with your actual data, please tell me with enough description and some examples of actual data. (At least, you need to show me a first few hundreds of the elements in the actual "logo" array.)

Replace
let logo: [String]
with
let logo: [Int]
to get errors use
do {
let classes = try JSONDecoder().decode([Brand].self, from: data)
}
catch {
print(error)
}

Related

Swift Decodable from JSON using wildcard keys [duplicate]

I have a JSON object with incrementing names to parse and I want to store the output into an object with a name field and a list of pet field. I normally use JSONDecoder as its pretty handy and easy to use, but I don't want to hard-code the CodingKey as I think it is very bad practice.
Input:
{"shopName":"KindHeartVet", "pet1":"dog","pet2":"hamster","pet3":"cat", ...... "pet20":"dragon"}
The object that I want to store the result in is something like the following.
class VetShop: NSObject, Decodable {
var shopName: String?
var petList: [String]?
private enum VetKey: String, CodingKey {
case shopName
case petList
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: VetKey.self)
shopName = try? container.decode(String.self, forKey: .shopName)
// implement storing of petList here.
}
}
What I'm struggling a lot on is, as CodingKey is enum, its a let constants, so I can't modify (and shouldn't modify) a constant, but I need to map the petList to the "petN" field, where N is the incrementing number.
EDIT :
I definitely cannot change the API response structure because it is a public API, not something I developed, I'm just trying to parse and get the value from this API, hope this clear the confusion!
Codable has provisions for dynamic keys. If you absolutely can't change the structure of the JSON you're getting, you could implement a decoder for it like this:
struct VetShop: Decodable {
let shopName: String
let pets: [String]
struct VetKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = "\(intValue)";
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: VetKeys.self)
var pets = [String]()
var shopName = ""
for key in container.allKeys {
let str = try container.decode(String.self, forKey: key)
if key.stringValue.hasPrefix("pet") {
pets.append(str)
} else {
shopName = str
}
}
self.shopName = shopName
self.pets = pets
}
}
You can try this to parse your data as Dictionary. In this way, you can get all keys of the dictionary.
let url = URL(string: "YOUR_URL_HERE")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
do {
let dics = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Dictionary<String, Any>
let keys = [String](dics.keys)
print(keys) // You have the key list
print(dics[keys[0]]) // this will print the first value
} catch let error as NSError {
print(error)
}
}).resume()
I hope you can figure out what you need to do.

parse JSON with swift via a model to object - decode int/string trouble

To get JSON from a website and turn it into an object is fairly simple using swift 4:
class func getJSON(completionHandler: #escaping (MyModel) -> Void)
{
let myUrl = "https://example.com/whatever"
if let myUrl = URL(string: myUrl)
{
URLSession.shared.dataTask(with: myUrl)
{ (data, response, err) in
if let data = data
{
do
{
let myData = try JSONDecoder().decode(MyModel.self, from: data)
completionHandler(myData)
}
catch let JSONerr
{
print("Error: \(JSONerr)")
}
}
return
}.resume()
}
}
MyModel is a data model:
struct MyModel
{
products: [MyProduct]
}
struct MyProduct
{
id: Int
...
I use this to GET from my WebService and it works well for most JSON structures.
However, I facing trouble with this complex JSON object. (By complex I mean too long to post here, so I hope you can figure-out such a pattern. Also the JSON object it has many nested arrays, etc.)
Eg.
{
"products" : [
{
"name" : "abc"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
},
{
"name" : "xyzzy"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
}]
}
(This may not be valid JSON - it is a simple extract of a larger part.)
The app crashes "...Expected to decode String but found a number
instead."!
So I change the model to use a 'String' in place of the
'Int'.
Again it crashes, saying "...Expected to decode Int but found
a string/data instead."!
So I change the model back, place an 'Int'
in place of the 'String'.
(The cycle repeats.)
It seems the value in question is sometimes an Int and sometimes a
String.
This NOT only happens with a certain key. I know of at least five other similar cases in this JSON.
So that means that I may get another error for another key, if a solution was only for that specific key. I would not be surprised to find many other cases as well.
QUESTION: How can I properly decode the JSON to my object, where the type of its elements can be either an Int or a String?
I want a solution that will either apply to all Model members or try convert a value to a String type if Int fails. Since I don't know which other keys will as fail.
You can use if lets to handle unpredictable values:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let memberValue = try? container.decode([String].self, forKey: .member){
stringArrayMember = memberValue
}
else if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
intMember = int
}
}
Or if it's a specific case of String vs Int and you'd like the same variable to handle the values, then something like:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
stringMember = String(int)
}
}
Edit
Your MyProduct will now look like:
struct MyProduct: Decodable {
var id: String?
var someOtherProperty: String?
enum MemberKeys: String, CodingKey {
case id
case someOtherProperty
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
someOtherProperty = try? container.decode(String.self, forKey: .someOtherProperty)
// Problematic property which can be String/Int
if let str = try? container.decode(String.self, forKey: .id){
id = str
}
else if let int = try? container.decode(Int.self, forKey: .id){
id = String(int)
}
}
}
Hope this helps.
This wasn't the problem that the error message gave!
All I needed to do to fix the problem was to employ CodingKeys.
I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.
Now an example of my model:
struct Product
{
let name: String
let long_name_value: String
...
enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"
...
}
}
I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

Swift 4 JSON Parsing Int - what am I doing wrong?

Swift 4 JSON Parsing Int issue. All the examples I've seen Int is encoded / decoded out of the box. Anyone see what I am doing wrong?
Thanks!
JSON
let patientBundleEntry = """
{
"resourceType": "Bundle",
"id": "patientListBundle",
"type": "SearchSet",
"total": 123
}
"""
Classes
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int? // this is printing -> Optional(105553116787496) instead of 123
}
class Resource:Codable {
var resourceType:ResourceType? // this is printing fine
}
Test - my assert for total being 123 fails and the optional is a long number. Any ideas why? Is my encoding wrong using .utf8??
func testModelBundle(){
let jsonDataEncoded:Data? = patientBundleEntry.data(using: .utf8)!
guard let responseData = jsonDataEncoded else {
print("Error: did not receive data")
}
do {
let bundleDecoded = try JSONDecoder().decode(BundleFHIR.self, from: responseData)
print("bundleDecoded.resourceType resource type \(bundleDecoded.resourceType )") //THIS is right
print("bundleDecoded.resourceType total \(bundleDecoded.total )") THIS is wrong
assert(bundleDecoded.total == 123, "something is wrong") // ***** <- this assert fails and it prints Optional(105553116787496)
} catch {
print("error trying to convert data to JSON")
}
}
At first you have to decode and then parse the JSON data.
Follow the below code :
struct Patient: Codable {
var resourceType: String
var id: String
var type: String
var total: Int
}
let json = patientBundleEntry.data(using: .utf8)!
let decoder = JSONDecoder()
let patient = try! decoder.decode(Patient.self, from: json)
print(patient.total) // Prints - 123
Ok there were a lot of issues with my code. Mainly that I didn't have codingKeys as private and therefore tried to rename it because the inheritance tree could not discern between the two. This caused me to not be implementing the true protocol. Not sure why it was half working... but here is my final code and it works great!
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int?
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
total = try values.decodeIfPresent(Int.self, forKey: .total)
entry = try values.decodeIfPresent([BundleEntry].self, forKey: .entry)
}
private enum CodingKeys: String, CodingKey
{
case total
case entry
}
}
class Resource:Codable {
var resourceType:ResourceType?
var id:String?
init(){
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
resourceType = try values.decode(ResourceType.self, forKey: .resourceType)
id = try values.decode(String.self, forKey: .id)
}
private enum CodingKeys: String, CodingKey
{
case resourceType
case id
}
}

Swift4, JSON, keyNotFound, No value associated with key

I need to do Sunset Sunrise App, and this is my code. But I have this error:
Error serializing json: keyNotFound(Sunrise_Sunset.SunPosition.Results.(CodingKeys in _B4291256871B16D8D013EC8806040532).sunrise, Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key sunrise (\"sunrise\").", underlyingError: nil))
And I don't understand how to fix it. Maybe someone had this problem. I will be grateful for any help)
This is the API, which I have: https://sunrise-sunset.org/api
struct SunPosition: Codable {
struct Results: Codable {
let sunrise: String
let sunset: String
let solarNoon: String
let dayLenght: String
let civilTwilightBegin: String
let civilTwilightEnd: String
let nauticalTwilightBegin: String
let nauticalTwilightEnd: String
let astronomicalTwilightBegin: String
let astronomicalTwilightEnd: String
enum CodingKey:String, Swift.CodingKey {
case sunrise = "sunrise"
case sunset = "sunset"
case solarNoon = "solar_noon"
case dayLenght = "day_length"
case civilTwilightBegin = "civil_twilight_begin"
case civilTwilightEnd = "civil_twilight_end"
case nauticalTwilightBegin = "nautical_twilight_begin"
case nauticalTwilightEnd = "nautical_twilight_end"
case astronomicalTwilightBegin = "astronomical_twilight_begin"
case astronomicalTwilightEnd = "astronomical_twilight_end"
}
}
}
extension SunPosition.Results {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
sunrise = try values.decode(String.self, forKey: .sunrise)
sunset = try values.decode(String.self, forKey: .sunset)
solarNoon = try values.decode(String.self, forKey: .solarNoon)
dayLenght = try values.decode(String.self, forKey: .dayLenght)
civilTwilightBegin = try values.decode(String.self, forKey: .civilTwilightBegin)
civilTwilightEnd = try values.decode(String.self, forKey: .civilTwilightEnd)
nauticalTwilightBegin = try values.decode(String.self, forKey: .nauticalTwilightBegin)
nauticalTwilightEnd = try values.decode(String.self, forKey: .nauticalTwilightEnd)
astronomicalTwilightBegin = try values.decode(String.self, forKey: .astronomicalTwilightBegin)
astronomicalTwilightEnd = try values.decode(String.self, forKey: .astronomicalTwilightEnd)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let sunPosition = try JSONDecoder().decode(SunPosition.Results.self, from: data)
print(sunPosition)
}catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
}
Four issues:
Add let results : Results in the SunPosition struct.
Typo: private enum CodingKeys: String, CodingKey rather than enum CodingKey :String, Swift.CodingKey, note the singular / plural difference, the private attribute is recommended but does not cause the issue.
Wrong type to decode: JSONDecoder().decode(SunPosition.self, from: data) rather than JSONDecoder().decode(SunPosition.Results.self, from: data).
To get the results you have to print(sunPosition.results).
Three notes:
Delete the entire extension. In this case you get the initializer for free.
Add &formatted=0 to the URL and set the dateDecodingStrategy of the decoder to .iso8601 to get Date objects. Change the type of all date related properties from String to Date and the type of dayLenght from String to TimeInterval. To change dateDecodingStrategy write
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let sunPosition = try decoder.decode(SunPosition.self ...
I recommend to handle the status, too. Add this in the SunPosition struct
let status : String
var success : Bool { return status == "OK" }
To rule it out, if for some reason your endpoint is returning an error message or nil data, you'll get this error.

Swift 4 Not decoding JSON optional properly

Swift noob here.
I'm trying to follow the App Development With Swift book and am running into trouble with decoding JSON data from the NASA API as given in the examples. Here's the code I'm trying to use:
struct PhotoInfo: Codable {
var title: String
var description: String
var url: URL
var copyright: String?
enum CodingKeys: String, CodingKey {
case title
case description = "explanation"
case url
case copyright
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
self.title = try valueContainer.decode(String.self, forKey: CodingKeys.title)
self.description = try valueContainer.decode(String.self, forKey: CodingKeys.description)
self.url = try valueContainer.decode(URL.self, forKey: CodingKeys.url)
self.copyright = try valueContainer.decode(String.self, forKey: CodingKeys.copyright)
}
}
func fetchPhotoInfo(completion: #escaping (PhotoInfo?) -> Void) {
let baseURL = URL(string: "https:/
let query: [String: String] = [
"api_key": "yN3**0scRWo12gCa25TWBcfp3rcuAnoeqwbpvLPn",
"date": "2011-07-13"
]
let url = baseURL.withQueries(query)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let photoInfo = try? jsonDecoder.decode(PhotoInfo.self, from: data) {
print(data)
completion(photoInfo)
} else {
print("Either no data was returned, or data was not properly decoded.")
completion(nil)
}
}
task.resume()
}
When I remove the copyright code from the PhotoInfo struct, it decodes the JSON and prints the data (line 36). Otherwise, it doesn't deserialize it. Is there a way I can troubleshoot why this is happening? Does it have something to do with the optional?
If copyright is optional , then you can make use of decodeIfPresent.
self.copyright = try valueContainer.decodeIfPresent(String.self, forKey: CodingKeys.copyright)
EDIT: Updated per #matt's comments.
technerd's answer is probably best, but I faced the same problem and dealt with it by changing try to try?. That's how it was presented in previous exercises, anyway. However, as #matt points out, this doesn't deal with the possibility that the copyright information isn't simply missing, but in fact something other than what was expected. Using try? in tandem with an if let statement provides at least basic feedback.