I have json data with the same core structure from a nosql database (PK, SK, attributes). The attributes part will be different depending on the value of SK.
Example:
[
{
"PK": "POLL#1544693DF0E88EC-3225-410E-B156-D13781B238F6",
"SK": "#METADATA#1544693DF0E88EC-3225-410E-B156-D13781B238F6",
"attributes": {
"latitude": "53.34589121858683",
"longitude": "-6.272215191675388",
"max_choices": 50,
"number": "1544693",
"poll_open": false,
}
},
{
"PK": "POLL#1544693DF0E88EC-3225-410E-B156-D13781B238F6",
"SK": "CHOICE#00a6ec5c-acc1-40f1-a087-31160d2cfc65",
"attributes": {
"distance": 790.95097525,
"latitude": 53.3416,
"price": "€€",
"categories": [
{
"title": "Ramen",
"alias": "ramen"
}
],
"vote_count": 0,
"longitude": -6.26274
}
}
]
Is it possible to use decode without errors? I've been stuck on this for hours.
I've defined the following:
struct Result: Codable {
var PK: String
var SK: String
var attributes: String
}
But, when I decode, I get the error:
typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil)], debugDescription: "Expected to decode String but found a dictionary instead.", underlyingError: nil))
I just want to decode 'attributes' as a generic string and parse it later depending on the value of SK when I know how to properly handle it. Why is this so difficult?
Do you need attributes right now? Or are you just looking for pk and sk? If you do not need it just do not include
var attributes: String
in your struct. It will not have a decode error and it will decode the other two, you just will not get the attributes parameter. It is not able to decode the attributes
as a string because it is not. It is really more like a dictionary. Swift does not know how to handle that unless you specifically tell it. That being said you could always do this
struct Result: Codable {
var PK: String
var SK: String
var attributes: Attributes
}
struct Attributes: Codable {
var lattitude: String
var longitude: String
//and whatever else you know you have
}
the key is only adding the values you know will be included in the attributes or else it will give an error
You need to handle this using JSONSerialization instead of Codable. Given you still want to use the same struct you need to change it to
struct Result {
var PK: String
var SK: String
var attributes: [String: Any]
}
and decode the json like this
var result: Result?
do {
if let dictionary = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let pk = dictionary["PK"] as? String,
let sk = dictionary["SK"] as? String,
let attributes = dictionary["attributes"] as? [String: Any] {
result = Result(PK: pk, SK: sk, attributes: attributes)
}
} catch {
print(error)
}
Now you still need to convert the attributes ([String: Any]) property of Result to something more usable but that is beyond this question
Related
I am new to Swift and I was assigned a task to decode this JSON:
{
"data": {
"id": 1,
"elements": {
"E01": {
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
"E02": {
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
}
}
}
I played around with this and I figured out how to decode this JSON when elements is an array of dictionaries with no incrementing key value. This is when elements looks like this:
"elements": [
{
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
{
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
]
Here is my code on how to decode the JSON when elements look like the JSON directly above:
The classes I am decoding to:
public class StructureContainer: Codable {
let data: DataStructure
init(data: DataStructure) {
self.data = data
}
}
public class DataStructure: Codable {
let id: Int64?
let elements: [ElementStructure]?
init(id: Int64?, elements: [ElementStructure]?) {
self.id = id
self.elements = elements
}
}
public class ElementStructure: Codable {
let title: String?
let items: [String]?
let details: DetailStructure?
init(title: String?, items: [String]?, details: DetailStructure?) {
self.title = title
self.items = items
self.details = details
}
}
public class DetailStructure: Codable {
var id: Int64?
var location: String?
init(id: Int64?, location: String?) {
self.id = id
self.location = location
}
}
How I am decoding this:
func parseJSONLocally() {
do {
// JSONText is the variable that contains the JSON string
let jsonData = JSONText.data(using: .utf8)!
// dataInstance is the variable of type Structure that stores the parsed object.
dataInstance = try JSONDecoder().decode(StructureContainer.self, from: jsonData)
}
catch {
print("error", error)
}
}
I am able to successfully decode this when elements is an array of dictionaries with no incrementing key. However, I am lost on how to decode this when elements is a dictionary of dictionaries with each key incrementing.
I have found similar questions to this on StackOverflow but I don't think I have seen my exact problem. I am also new to Swift so I might be missing some knowledge somewhere. Any help is appreciated.
First of all Int64 makes no sense. On all modern computers Int64 is equal to Int. And declare only the properties as optional which can be nil
Assuming the dictionary keys don't matter implement init(from decoder to decode the dictionary version, decode [String:ElementStructure] and assign the dictionary values sorted by the keys to elements
public class DataStructure: Decodable {
let id: Int
let elements: [ElementStructure] // why not just Element?
private enum CodingKeys : String, CodingKey { case id, elements }
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let elementData = try container.decode([String:ElementStructure].self, forKey: .elements)
let sortedkeys = elementData.keys.sorted{ $0.localizedStandardCompare($1) == .orderedAscending }
elements = sortedkeys.map{elementData[$0]!}
}
}
I am trying to create a small app to control my Hue lights with SwiftUI, but I just can't manage to decode/iterate through this JSON. There are so many threads on how to do this and I have tried tens of them, using CodingKeys, creating custom decoders, and so on, but I just can't seem to get it. So this is the JSON I am getting from my Hue Bridge:
{
"1": {
"state": {
"on": true,
"bri": 254,
"hue": 8417,
"sat": 140,
"effect": "none",
"xy": [
0.4573,
0.41
],
"ct": 366,
"alert": "select",
"colormode": "ct",
"mode": "homeautomation",
"reachable": false
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-26T12:56:12"
},
...
},
"2": {
"state": {
"on": false,
"bri": 137,
"hue": 36334,
"sat": 203,
"effect": "none",
"xy": [
0.2055,
0.3748
],
"ct": 500,
"alert": "select",
"colormode": "xy",
"mode": "homeautomation",
"reachable": true
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-13T12:29:48"
},
...
},
"9": {
"state": {
"on": false,
"bri": 254,
"hue": 16459,
"sat": 216,
"effect": "none",
"xy": [
0.4907,
0.4673
],
"alert": "none",
"colormode": "xy",
"mode": "homeautomation",
"reachable": true
},
"swupdate": {
"state": "noupdates",
"lastinstall": "2021-08-12T12:51:18"
},
...
},
...
}
So the structure is basically a dynamic key on the root level and the light info, that is the same. I have created the following types:
struct LightsObject: Decodable {
public let lights: [String:LightInfo]
}
struct LightInfo: Decodable {
var state: StateInfo
struct StateInfo: Decodable {
var on: Bool
var bri: Int
var hue: Int
var sat: Int
var effect: String
var xy: [Double]
var ct: Int
var alert: String
var colormode: String
var reachable: Bool
}
var swupdate: UpdateInfo
struct UpdateInfo: Decodable {
var state: String
var lastinstall: Date
}
...
(it basically continues to include all the variables from the object)
The problem I have now, is that I can't seem to get this into a normal array or dictionary. I would settle for something like {"1":LightInfo, "2", LightInfo}, where I could iterate over, or just a simple [LightInfo, LightInfo, ...], because I may not even need the index.
And then, ideally, I could do something like
ForEach(lights) { light in
Text(light.name)
}
I have tried creating a custom coding key, implementing the type as Codable, and so on, but I couldn't find a solution, that works for me. I know, there are a lot of threads on this topic, but I feel that my initial setup might be wrong and that's why it's not working.
This is the decoding part:
let task = urlSession.dataTask(with: apiGetLightsUrl, completionHandler: { (data, response, error) in
guard let data = data, error == nil else { return }
do {
let lights = try JSONDecoder().decode([String: LightInfo].self, from: data)
completionHandler(lights, nil)
} catch {
print("couldn't get lights")
completionHandler(nil, error)
}
})
I am actually getting the JSON, no problem, but I have not been able to decode it, yet, as I said. The latest error being:
Optional(Swift.DecodingError.typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "1", intValue: nil), CodingKeys(stringValue: "swupdate", intValue: nil), CodingKeys(stringValue: "lastinstall", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil)))
I have seen some posts, but the JSON they were working with usually had a top-level object, something like
{
"foo":
{
"bar": "foobar"
...
},
{
"bar": "foobar2"
...
},
{
"bar": "foobar3"
...
}
...
}
So the handling of that was a little different, since I could just create a struct like
struct Object: Decodable {
var foo: [LightsInfo]
}
which is not possible here :(
Can anybody point me in the right direction? Thanks!
Update:
Thanks guys, the solutions do work. I had some errors in my struct, which are now fixed. (misspelled variable names, optional values and so on). The only thing missing now, is how to loop through the dictionary. If I use
ForEach(lights)...
Swift complains, that it is only supposed to be used for static Dictionaries. I tried doing this
ForEach(lights, id: \.key) { key, value in
Button(action: {print(value.name)}) {
Text("foo")
}
}
but I get this error: Generic struct 'ForEach' requires that '[String : LightInfo]' conform to 'RandomAccessCollection'
Update 2:
So this seems to work:
struct LightsView: View {
#Binding var lights: [String:LightInfo]
var body: some View {
VStack {
ForEach(Array(lights.keys.enumerated()), id: \.element) { key, value in
LightView(lightInfo: self.lights["\(value)"]!, lightId: Int(value) ?? 0)
}
}
}
}
I'll try to clean up the code and optimize it a bit. Open for suggestions ;)
You seem to be almost there. This is a [String:LightInfo]. You just need to decode that (rather than wrapping that up in a LightsObject that doesn't exist in the JSON). You can pull off the values if you don't care about the numbers. It should be like this:
let lights = try JSONDecoder().decode([String: LightInfo].self, from: data).values
I ran into a similar problem myself where I wanted to handle generic keys given. This is how I solved it, adapted to your code.
struct LightsObject: Decodable {
public var lights: [String:LightInfo]
private enum CodingKeys: String, CodingKey {
case lights
}
// Decode the JSON manually
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.lights = [String:LightInfo]()
if let lightsSubContainer = try? container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .lights) {
for key in lightsSubContainer.allKeys {
if let lightInfo = try? lightsSubContainer.decode(LightInfo.self, forKey: key) {
self.lights?[key.stringValue] = lightInfo
}
}
}
}
}
public class GenericCodingKeys: CodingKey {
public var stringValue: String
public var intValue: Int?
required public init?(stringValue: String) {
self.stringValue = stringValue
}
required public init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
}
If you are only interested in the values of the root dictionary add a custom initializer
struct LightsObject: Decodable {
let lights: [LightInfo]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String:LightInfo].self)
lights = Array(dictionary.values)
}
}
and decode
let result = try JSONDecoder().decode(LightsObject.self, from: data)
completionHandler(result.lights, nil)
which returns an array of LightInfo objects and you can declare
#Binding var lights: [LightInfo]
Of course the completion handler type must be ([LightInfo]?, Error?) -> Void. Anyway consider to use the more convenient Result type
I am writing a Swift 5.x app using Alamofire 5 to get a list of files from an API I wrote. The API returns the file list as a JSON Data object. I want to then get this data into a struct I created. I am not able to get this working. Here is an example JSON string that my server sends over when you hit the API endpoint:
[{
"ID": "0d37ee7a-39bf-4eca-b3ec-b3fe840500b5",
"Name": "File01.iso",
"Size": 6148
}, {
"ID": "656e0396-257d-4604-a85c-bdd0593290cd",
"Name": "File02.iso",
"Size": 224917843
}, {
"ID": "275fdf66-3584-4899-8fac-ee387afc2044",
"Name": "File04.iso",
"Size": 5549504
}, {
"ID": "1c73f857-56b5-475b-afe4-955c9d2d87fe",
"Name": "File05.iso",
"Size": 15476866871
}, {
"ID": "bfebbca2-49de-43d7-b5d0-3461b4793b62",
"Name": "File06.iso",
"Size": 37254264
}]
I created the following Data Model in swift to hold this:
struct Files: Decodable {
let root: [File]
}
struct File: Decodable, Identifiable {
var id: UUID
var name: String
var size: Int
}
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
case size = "Size"
}
Then I used Alamofire 5.x to call the API endpoint and attempt to decode the JSON and place it into the object in question:
func getPackageFilesJSON() {
AF.request("http://localhost:8080/api/v1/list/pkgs").responseDecodable(of: Files.self) { response in
guard let serverFiles = response.value else {
print("Error Decoding JSON")
return
}
let self.serverFilesList = serverFiles
}
}
This fails. If I debugPrint the response I get this for the result:
[Result]: failure(Alamofire.AFError.responseSerializationFailed(reason: Alamofire.AFError.ResponseSerializationFailureReason.decodingFailed(error: Swift.DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)))))
I have never been great at creating these data models and decoding JSON into them. I am sure I am missing something silly. I am hopeful that someone more knowledgable than me, or a second set of eyes can help me get this working.
Thanks,
Ed
There is no key root in the JSON. The root object is an array
Delete
struct Files: Decodable {
let root: [File]
}
and decode
AF.request("http://localhost:8080/api/v1/list/pkgs").responseDecodable(of: [File].self) { response in ...
and move the CodingKeys enum into the File struct
struct File: Decodable, Identifiable {
var id: UUID
var name: String
var size: Int
enum CodingKeys: String, CodingKey {
case id = "ID"
case name = "Name"
case size = "Size"
}
}
I'm trying to parse the JSON at a certain API endpoint using the Codable protocol in Swift. How do you properly decode this?
So far, I've created a Model struct to represent the Post objects located at the endpoint. I'm also using JSONDecoder() to decode the array of Post objects. I've tried restructuring my Model to have nested structs but that doesn't work.
Here's a sample of the JSON:
[
{
"ID": 1,
"title": "Title 1",
"content": "Content 1"
...
},
{
"ID": 2,
"title": "Title 2",
"content": "Content 2"
...
}
]
Here's the Model:
struct Post: Codable {
let id: Int
let title, content: String
enum CodingKeys: String, CodingKey {
case id = "ID"
case title, content
}
}
Here's a sample of the fetching code:
let task = URLSession.shared.dataTask(with: url) {
data, _, error in
if let error = error {
print("Error: Failed to fetch data from url. [\(error.localizedDescription)]")
completion(.failure(error))
return
}
guard let data = data else {
print("Data not available.")
return
}
do {
let decoder = JSONDecoder()
let posts = try decoder.decode([Post].self, from: data)
print("POSTS: : ", posts)
// ^ THIS IS NOT WORKING. I've tried Post.self too.
} catch let jsonError {
//error
}
}
task.resume()
Here's the error I'm getting:
ERROR: keyNotFound(CodingKeys(stringValue: "ID", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"ID\", intValue: nil) (\"ID\").", underlyingError: nil))
Your code is just fine. You just need to check your json data. This error shows in the case:
When certain key in Coding keys doesn't match with the key of json data. In your case it is clearly = "ID"
To solve this check response in postMan and make the keys exactly same in Codingkeys
I have to decode this type of JSON that is downloaded by a response.
The JSON is this, I need retrieve the "gallery" of all items
JSON: https://pastebin.com/KnEwZzxd
I have tried many solution but I am not able to create a Decode of this son.
I have posted the full code on pastebin, too.
{
"status": 200,
"data": {
"date": "2018-07-29T00:00:00.300Z",
"featured": [
{
"id": "5b56298d781e197186378f50",
"name": "Sun Tan Specialist",
"price": "1,500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/icon.png",
"png": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/png.png",
"gallery": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/gallery.jpg",
"featured": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/featured.png"
},
"rarity": "epic",
"type": "outfit",
"readableType": "Outfit"
},
{
"id": "5b562af2781e19db65378f5c",
"name": "Rescue Paddle",
"price": "800",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/icon.png",
"png": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/png.png",
"gallery": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/gallery.jpg",
"featured": false
},
"rarity": "rare",
"type": "pickaxe",
"readableType": "Pickaxe"
}
],
"daily": [
{
"id": "5ab1723e5f957f27504aa502",
"name": "Rusty Rider",
"price": "1,200",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/icon.png",
"png": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/png.png",
"gallery": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/gallery.jpg",
"featured": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/featured.png"
},
"rarity": "epic",
"type": "glider",
"readableType": "Glider"
},
{
"id": "5b0e944bdb94f1a4bbc0a8e4",
"name": "Rambunctious",
"price": "500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/icon.png",
"png": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/png.png",
"gallery": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/gallery.jpg",
"featured": false
}
]
}
}
Aside from posting the JSON Code itself, it would be useful to actually show an attempt as to how you have attempted to decode it as well ^_________^.
Anyway, the best way to tackle this issue is to use custom Structs and the Decodable Protocol to handle the JSON response.
From your JSON you will initially get a two values:
/// The Initial Response From The Server
struct Response: Decodable {
let status: Int
let data: ResponseData
}
From this we then map the 'data' to a struct called ResponseData:
/// The Data Object
struct ResponseData: Decodable{
let date: String
let featured: [Product]
let daily: [Product]
}
In this we have two variables which contain an array of identical struct which I have called Product:
/// The Product Structure
struct Product: Decodable{
let id: String
let name: String
let price: String
let priceIcon: String
let priceIconLink: String
let images: ProductImages
let rarity: String
let type: String
let readableType: String
}
Within this we have one variable which is a dictionary (images) which we then map to another struct:
/// The Data From The Product Images Dictionary
struct ProductImages: Decodable{
let icon: String
let png: String
let gallery: String
///The Featured Variable For The Product Images Can Contain Either A String Or A Boolean Value
let featured: FeaturedType
}
The issue you have with the ProductImages, is that the featured var sometimes contains a String but on others it contains a Bool value. As such we need to create a custom struct to handle the decoding of this to ensure we always get a String (I am probably not doing this right way so if someone has a better solution please say so):
/// Featured Type Returns A String Of Either The Boolean Value Or The Link To The JPG
struct FeaturedType : Codable {
let formatted: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
//1. If We Get A Standard Response We Have A String
let stringResult = try container.decode(String.self)
formatted = stringResult
} catch {
//2. On Occassions We Get An Bool
let boolResult = try container.decode(Bool.self)
formatted = String(boolResult)
}
}
}
Now that is the basic structure of your JSON so now you need to handle it. In this example I am loading the JSON from the MainBundle as I dont have the actual URL.
/// Loads & Decodes The JSON File
func retreiveJSON(){
//1. Load The JSON File From The Main Bundle
guard let jsonURL = Bundle.main.url(forResource: "sample", withExtension: ".json") else { return }
do{
//2. Get The Data From The URL
let data = try Data(contentsOf: jsonURL)
//3. Decode The JSON
let jsonData = try JSONDecoder().decode(Response.self, from: data)
//4. Extract The Data
extractDataFrom(jsonData)
}catch{
print("Error Processing JSON == \(error)")
}
}
In the above function you will notice I am calling the function extractDataFrom() which allows you to then do what you need to do with your data:
/// Extracts The Daily & Featured Products From The JSON
///
/// - Parameter jsonData: Response
func extractDataFrom(_ jsonData: Response){
//1. Get The Daily Products
let dailyProducts = jsonData.data.daily
dailyProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
//2. Get The Featured Products
let featuredProducts = jsonData.data.featured
featuredProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
}
If you wanted to save this data then all you would need to do is add the following variables under your class declaration e.g:
var featuredProducts = [Product]()
var dailyProducts = [Product]()
And in the extractDataFrom() function change the:
let dailyProducts
let featuredProducts
To:
dailyProducts = jsonData.data.daily
featuredProducts = jsonData.data.featured
Please note that this is a very crude example, and as noted, I may not be handling the 'featured' variable correctly.
Hope it helps...
Thanks to quicktype and other services converting valid json to Swift and other languages is simple. Editing it to fit your needs should be simple enough.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
struct Welcome: Codable {
let status: Int
let data: DataClass
}
struct DataClass: Codable {
let date: String
let featured, daily: [Daily]
}
struct Daily: Codable {
let id, name, price, priceIcon: String
let priceIconLink: String
let images: Images
let rarity, type, readableType: String?
}
struct Images: Codable {
let icon, png, gallery: String
let featured: Featured
}
enum Featured: Codable {
case bool(Bool)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Featured.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Featured"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}