I'm just learning the Swift Decodable protocol and am running into a problem. I am able to decode one json object into a swift object, but am stuck with decoding an array.
What goes well:
imagine following json:
let json = """
{
"all" : {
"_id": "59951d5ef2db18002031693c",
"text": "America’s cats, including housecats that adventure outdoors and feral cats, kill between 1.3 billion and 4.0 billion birds in a year.",
"type": "cat",
"user": {
"_id": "5a9ac18c7478810ea6c06381",
"name": {
"first": "Alex",
"last": "Wohlbruck"
}
},
"upvotes": 4,
"userUpvoted": null
}
}
"""
let jsonData = json.data(using: .utf8)
I can decode it to a Fact object with following code:
enum Type: String, Decodable {
case cat = "cat"
}
struct Fact {
let id: String
let text: String
let type: Type
let upvotes: Int
enum CodingKeys: CodingKey {
case all
}
enum FactKeys: CodingKey {
case _id, text, type, upvotes
}
}
extension Fact: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let allContainer = try container.nestedContainer(keyedBy: FactKeys.self, forKey: .all)
id = try allContainer.decode(String.self, forKey: ._id)
text = try allContainer.decode(String.self, forKey: .text)
type = try allContainer.decode(Type.self, forKey: .type)
upvotes = try allContainer.decode(Int.self, forKey: .upvotes)
}
}
let decoder = JSONDecoder()
let fact = try decoder.decode(Fact.self, from: jsonData!)
But the api is giving me an array of objects:
let json = """
{
"all": [
{
"_id": "59951d5ef2db18002031693c",
"text": "America’s cats, including housecats that adventure outdoors and feral cats, kill between 1.3 billion and 4.0 billion birds in a year.",
"type": "cat",
"user": {
"_id": "5a9ac18c7478810ea6c06381",
"name": {
"first": "Alex",
"last": "Wohlbruck"
}
},
"upvotes": 4,
"userUpvoted": null
},
{
"_id": "5b01a447c6914f0014cc9a30",
"text": "The special sensory organ called the Jacobson's organ allows a cat to have 14 times the sense of smell of a human. It consists of two fluid-filled sacs that connect to the cat's nasal cavity and is located on the roof of their mouth behind their teeth.",
"type": "cat",
"user": {
"_id": "5a9ac18c7478810ea6c06381",
"name": {
"first": "Alex",
"last": "Wohlbruck"
}
},
"upvotes": 4,
"userUpvoted": null
}
]
}
"""
let jsonData = json.data(using: .utf8)
And I want to store that in an allFacts array that hold my Fact objects
class Facts: ObservableObject {
#Published var allFacts = [Fact]()
}
let decoder = JSONDecoder()
let allFacts = try decoder.decode([Fact].self, from: jsonData!)
I'm using the same extension on my Fact struct. But it's giving me an error and I am totally lost for a second. Any idea how I can solve this ?
Do I need to create codingKeys for the class as well ?
Expected to decode Array<Any> but found a dictionary instead."
I recommend not to mess around with nested containers. This is less efficient than the default stuff. In your case you would have to use nestedUnkeyedContainer and iterate the array which is still more expensive.
Instead just add a root struct
let json = """
{
"all": [
{
"_id": "59951d5ef2db18002031693c",
"text": "America’s cats, including housecats that adventure outdoors and feral cats, kill between 1.3 billion and 4.0 billion birds in a year.",
"type": "cat",
"user": {
"_id": "5a9ac18c7478810ea6c06381",
"name": {
"first": "Alex",
"last": "Wohlbruck"
}
},
"upvotes": 4,
"userUpvoted": null
},
{
"_id": "5b01a447c6914f0014cc9a30",
"text": "The special sensory organ called the Jacobson's organ allows a cat to have 14 times the sense of smell of a human. It consists of two fluid-filled sacs that connect to the cat's nasal cavity and is located on the roof of their mouth behind their teeth.",
"type": "cat",
"user": {
"_id": "5a9ac18c7478810ea6c06381",
"name": {
"first": "Alex",
"last": "Wohlbruck"
}
},
"upvotes": 4,
"userUpvoted": null
}
]
}
"""
let jsonData = Data(json.utf8)
enum Type: String, Decodable {
case cat
}
struct Root : Decodable {
let all : [Fact]
}
struct Fact : Decodable {
let id: String
let text: String
let type: Type
let upvotes: Int
enum CodingKeys : String, CodingKey {
case id = "_id", text, type, upvotes
}
}
let decoder = JSONDecoder()
let root = try decoder.decode(Root.self, from: jsonData)
print(root.all)
Related
I get the response from the api as in the JSON code below
{
"timestamp": 1632838801,
"status": "ok",
"message": "ok",
"data": [
{
"id": 1,
"id_category": 6,
"products": [
{
"id": 12,
"product_name": "product one",
}
]
},
{
"id": 2,
"id_category": 6,
"products": [
{
"id": 20,
"product_name": "product two"
}
]
},
{
"id": 3,
"id_category": 13,
"products": [
{
"id": 994,
"product_name": "product three"
}
]
}
]
}
I'm success to decode the response into a struct like this:
struct ProductDataResponse {
let timestamp: Int
let status: Int
let message: String
let data: [Data]
}
struct Data {
let id: Int
let idCategory: Int
let products: [Product]
}
struct Product {
let id: Int
let productName: String
let idJelajah: Int
}
the problem is, I want my data model different from the response, if the data has the same id_cateogry then the existing product data will be grouped into the same id_category,below i attach the expected result of struct and the expected json.
the result is 2 array of diffrent id_category and product
struct ListCategory {
let idCategory
let category: [Category]
}
struct Category {
var id: Int
var product: [Product]
}
struct Product {
let id: Int
let productName: String
let idJelajah: Int
}
{
"timestamp": 1632838801,
"status": "ok",
"message": "ok",
"data": [
{
"id_category": 6,
"products": [
{
"id": 12,
"product_name": "product one",
},
{
"id": 20,
"product_name": "product two"
}
]
},
{
"id_category": 13,
"products": [
{
"id": 994,
"product_name": "product three"
}
]
},
]
}
With model being:
struct ProductDataResponse: Codable {
let timestamp: Double
let status: String
let message: String
let data: [Data]
struct Data: Codable {
let id: Int
let idCategory: Int
let products: [Product]
}
struct Product: Codable {
let id: Int
let productName: String
}
}
struct ProductDataOutput: Codable {
let timestamp: Double
let status: String
let message: String
let data: [Category]
struct ListCategory: Codable {
let idCategory: Int
let category: [Category]
}
struct Category: Codable {
var id: Int
var product: [Product]
}
struct Product: Codable {
let id: Int
let productName: String
}
}
Side note, to be working with your current code, I changed timestamp into a Double & status into a String, else it would cause decoding issue.
I also put them inside each other, to avoid naming. For instance, you'd need to differentiate Swift.Data & ProductDataResponse.Data, and the two Products would collide.
Also, I added a top level structure that you didn't give: ProductDataOutput.
And a helper for converting Product into Product (ie the one with structure of the response, and the one with the target one). Of course, we could use the same struct, but I wanted to separate them, just in case as you wrote it yourself.
extension ProductDataOutput.Product {
init(with otherProduct: ProductDataResponse.Product) {
self.id = otherProduct.id
self.productName = otherProduct.productName
}
}
The following code is inside a
do {
//HERE
} catch {
print("Error: \(error)")
}
That I'll skip, but keep it for debugging and catch issue.
Let's parse into the model as the Response first:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let parsed = try decoder.decode(ProductDataResponse.self, from: Data(initialJSONStr.utf8))
print(parsed)
Then, I'd use reduce(into:_:) to group by category id the products.
let reduced = parsed.data.reduce(into: [Int: [ProductDataOutput.Product]]()) { partialResult, current in
var existing = partialResult[current.idCategory, default: []]
let newProducts = current.products.map { ProductDataOutput.Product(with: $0) }
existing.append(contentsOf: newProducts)
partialResult[current.idCategory] = existing
}
print(reduced)
Then, let's create our target structure. I used a quick map() to transform the reduced into a [ProductDataOutput.Category].
let output = ProductDataOutput(timestamp: parsed.timestamp,
status: parsed.status,
message: parsed.message,
data: reduced.map { ProductDataOutput.Category(id: $0.key, product: $0.value) })
Then, to debug the output as JSON (as it's the sample you gave):
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
let encodedOutput = try encoder.encode(output)
print(String(data: encodedOutput, encoding: .utf8)!)
With this, you should be fine.
I am trying to assign the result that is retrieved by Alamofire to an array, and have come across an issue.
I am using the Stripe API for products, it returns back a JSON object called "data:", I am trying to assign that data object only to that products array I have.
Here's my code
var products: [Product] = []
let stripeProducts = "stripe-products-api"
func createArray() {
let stripeAuthHeader: HTTPHeaders = []
AF.request(stripeProducts, headers: stripeAuthHeader).responseJSON {
switch response.result {
case .failure(let error):
print(error)
case .success:
self.products = response.data \\ trying to access the data object from the JSON data
print(self.products)
}
}
}
JSON:
"object": "list",
"data": [
{
"id": "prod_123456",
"object": "product",
"active": true,
"attributes": [
],
"created": 1590423835,
"description": "Test",
"images": [
""
],
"livemode": false,
"metadata": {
"address": "Test"
},
"name": "Blue Jeans",
"statement_descriptor": null,
"type": "service",
"unit_label": null,
"updated": 1590653248
}
]
Thank you for your help.
You need to have Struct of Products
struct Datum: Decodable {
let id, object: String
let active: Bool
let created: Int
let datumDescription: String
let images: [String]
let livemode: Bool
let metadata: Metadata
let name: String
let type: String
let updated: Int
enum CodingKeys: String, CodingKey {
case id, object, active, created
case datumDescription = "description"
case images, livemode, metadata, name
case type
case updated
}
}
// MARK: - Metadata
struct Metadata: Decodable {
let address: String
}
Then parse it like this
let products = try? newJSONDecoder().decode(Products.self, from: response.data)
I'm having trouble getting the direction values from the following JSON:
"routeOptions": [
{
"name": "Jubilee",
"directions": [
"Wembley Park Underground Station",
"Stanmore Underground Station"
],
"lineIdentifier": {
"id": "jubilee",
"name": "Jubilee",
"uri": "/Line/jubilee",
"type": "Line",
"routeType": "Unknown",
"status": "Unknown"
}
}
]
I believe the directions is a JSON array, which at the moment I'm using Codable as below. I've managed to get the routeOptions name but can't seem to figure out how to get the directions as there's no specific key variable. Please can someone help?
struct RouteOptions: Codable {
let name: String?
let directions: [Directions]?
init(name: String, directions: [Directions]) {
self.name = name
self.directions = directions
}}
struct Directions: Codable {}
You need to handle directions as an array of String
struct RouteOptions: Codable {
let name: String
let directions: [String]
}
Here is an example where I fixed the json to be correct
let data = """
{ "routeOptions": [
{
"name": "Jubilee",
"directions": [
"Wembley Park Underground Station",
"Stanmore Underground Station"
],
"lineIdentifier": {
"id": "jubilee",
"name": "Jubilee",
"uri": "/Line/jubilee",
"type": "Line",
"routeType": "Unknown",
"status": "Unknown"
}
}
]}
""".data(using: .utf8)!
struct Root: Decodable {
let routeOptions: [RouteOptions]
}
struct RouteOptions: Codable {
let name: String
let directions: [String]
}
do {
let result = try JSONDecoder().decode(Root.self, from: data)
print(result.routeOptions)
} catch {
print(error)
}
So basically I'm trying to read a local json file about some spendings. I have a struct "Spending" and a struct "Spendings" that holds an array of Spending. I can't access the data from my json when I decode with the type Spendings.
I tried to decode with [Spending.self] which is working but I want to use my struct Spendings and I can't figure why it doesn't work?
[
{
"id": 1,
"name": "Métro 052",
"price": 8.97,
"date": "22/07/2019",
"category": "Transport"
},
{
"id": 2,
"name": "National Geographic Museum",
"price": 10.77,
"date": "22/07/2019",
"category": "Museum"
}
]
enum Categories: String, Codable {
case Transport
case Food
case Museum
case Mobile
case Housing
case Gifts
case Shopping
}
struct Spending: Codable {
var id: Int
var name: String
var price: Float
var date: String
var category: Categories
}
struct Spendings: Codable {
let list: [Spending]
}
//Not working
class SpendingController {
static let shared = SpendingController()
func fetchSpendings(completion: #escaping ([Spending]?) -> Void) {
if let filepath = Bundle.main.path(forResource: "spending", ofType: "json") {
let jsonDecoder = JSONDecoder()
if let data = try? Data(contentsOf: URL(fileURLWithPath: filepath)), let spendings = try? jsonDecoder.decode(Spendings.self, from: data) {
completion(spendings.list)
}
}
}
}
//Working
class SpendingController {
static let shared = SpendingController()
func fetchSpendings(completion: #escaping ([Spending]?) -> Void) {
if let filepath = Bundle.main.path(forResource: "spending", ofType: "json") {
let jsonDecoder = JSONDecoder()
if let data = try? Data(contentsOf: URL(fileURLWithPath: filepath)), let spendings = try? jsonDecoder.decode([Spending].self, from: data) {
completion(spendings)
}
}
}
}
I don't have any error messages but in my completion when I print the result nothing is printed contrary to when I use [Spending].self.
Decoding a [Spending].self is indeed correct here because the root of your JSON is an array, which means that the type you use to decode should be [XXX].self.
Decoding a Spendings.self would be incorrect here because it would mean that you are a decoding an object root, as opposed to an array root. The Spendings struct has a single property list, so the JSON's root object would need to have a key of "list" in order for decoding Spendings.self to work correctly, like this:
{
"list":
[
{
"id": 1,
"name": "Métro 052",
"price": 8.97,
"date": "22/07/2019",
"category": "Transport"
},
{
"id": 2,
"name": "National Geographic Museum",
"price": 10.77,
"date": "22/07/2019",
"category": "Museum"
}
]
}
I want to parse JSON which has the following format:
{
"list": {
"q": "cookie",
"sr": "28",
"ds": "any",
"start": 0,
"end": 50,
"total": 6536,
"group": "",
"sort": "r",
"item": [
{
"offset": 0,
"group": "Branded Food Products Database",
"name": "THE COOKIE DOUGH CAFE, GOURMET EDIBLE COOKIE DOUGH, COOKIES & CREAM, UPC: 850947006012",
"ndbno": "45095905",
"ds": "BL"
},
{
"offset": 1,
"group": "Branded Food Products Database",
"name": "MELK AND COOKIES, COOKIE DOUGH CLUSTERS CHOCOLATE CHIP COOKIES, UPC: 094922378675",
"ndbno": "45026487",
"ds": "BL"
}]
}
I want to get the data in two classes. One class for the items I get in the JSON file and one for the list.
The item class should have the variables offset, group, name, ndbno and ds. Then I want to get all items to an array "items".
The class list would have the variables q, sr, ds, start, end, total, group, sort and the array items.
I want the json structure in the class structure I defined.
Someone knows how to do that? Thanks.
I get my json from a website. The request I get with the following code:
//Send Request
var done = false
print("Send request")
var requestResponse = ""
var parsedResults: AnyObject?
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {// check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
}
let responseString = String(data: data, encoding: .utf8)
done = true
requestResponse = responseString!
//print("responseString = \(responseString)")
}
task.resume()
//Wait for response
repeat {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
} while !done
It's pretty straightforward in Swift 4
let jsonString = """
{
"list": {
"q": "cookie",
"sr": "28",
"ds": "any",
"start": 0,
"end": 50,
"total": 6536,
"group": "",
"sort": "r",
"item": [
{
"offset": 0,
"group": "Branded Food Products Database",
"name": "THE COOKIE DOUGH CAFE, GOURMET EDIBLE COOKIE DOUGH, COOKIES & CREAM, UPC: 850947006012",
"ndbno": "45095905",
"ds": "BL"
},
{
"offset": 1,
"group": "Branded Food Products Database",
"name": "MELK AND COOKIES, COOKIE DOUGH CLUSTERS CHOCOLATE CHIP COOKIES, UPC: 094922378675",
"ndbno": "45026487",
"ds": "BL"
}
]
}
}
"""
struct Root : Decodable {
let list : List
struct List : Decodable {
let q : String
let sr : String
let ds : String
let start : Int
let end : Int
let total : Int
let group : String
let sort : String
let item : [Item] // should be `items`
struct Item : Decodable {
let offset : Int
let group : String
let name : String
let ndbno : String
let ds : String
}
}
}
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Root.self, from: data)
print(result)
} catch { print(error) }
PS: As already mentioned in the comments: The while loop is awful. Use an asynchronous completion handler