swift change struct model into another model if have similar id - arrays

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.

Related

How to make JSON from nested struct

I have a nested struct:
struct Order: Codable {
let foodList: [FoodList]
let name: String
let phone: String
let email: String
}
struct FoodList: Codable {
let foodName: String
let foodID: String
let foodAmount: Int
}
I need to make JSON from this struct. It have to be like this:
{
"name": "Name",
"phone": "+1 999 999 999 999",
"email": "email#email.email",
"foodList": [
{
"foodName": "Big Mac",
"foodID": "115",
"foodAmount": 2
},
{
"foodName": "Naggets",
"foodID": "221",
"foodAmount": 5
}
]
}
But I always get this JSON )
[
{
"email": "Name",
"phone": "+1 999 999 999 999",
"foodList": {
"foodAmount": 2,
"foodID": "115",
"foodName": "Big Mac"
},
"name": "email#email.email"
},
{
"email": "email#email.email",
"phone": "+1 999 999 999 999",
"foodList": {
"foodAmount": 5,
"foodID": "221",
"foodName": "Naggets"
},
"name": "Name"
}
]
In JSON it have to be 1 Person and array of selected food.
I code JSON to struct with this code:
let orderList = [OrderList]()
for index in 0..<subMenuList.count {
if subMenuList[index].foodList.foodSelectedAmount != 0 {
orderList.append(OrderList(
foodList: FoodListOrder(
foodName: subMenuList[index].foodList.foodName,
foodID: subMenuList[index].foodList.foodID,
foodAmount: subMenuList[index].foodList.foodSelectedAmount),
name: mainView.nameTextField.text!,
phone: mainView.phoneTextField.text!,
email: mainView.emailTextField.text!))
}
let jsonEncoder = JSONEncoder()
do {
let jsonData = try jsonEncoder.encode(orderList)
print(String(data: jsonData, encoding: .utf8)!)
} catch {
print(error)
}
Something wrong in "FOR" block and textfields... I think. I am appending person's data from textfields each time it loops.
Assuming Order is equal to OrderList and FoodList is equal to FoodListOrder you have to create first the FoodList array and then create the (single) Order object.
var foodList = [FoodList]()
for item in subMenuList { // don't use an index based loop if you actually don't need the index
if item.foodList.foodSelectedAmount != 0 {
foodList.append(FoodList(foodName: item.foodList.foodName,
foodID: item.foodList.foodID,
foodAmount: item.foodList.foodSelectedAmount)
}
}
let order = Order(foodList: foodList,
name: mainView.nameTextField.text!,
phone: mainView.phoneTextField.text!,
email: mainView.emailTextField.text!)
let jsonEncoder = JSONEncoder()
do {
let jsonData = try jsonEncoder.encode(order)
print(String(data: jsonData, encoding: .utf8)!)
} catch {
print(error)
}

Assigning Alamofire result to array

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)

Decode a JSON array

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)

I don't understand why my code works for an array of my struct but not with a struct containing an array of this struct?

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"
}
]
}

Swift Codable: Array of Dictionaries

I have a JSON object from Yelp and I cannot figure out how to access the title in categories using Swift Codable. This is the JSON (with some elements removed for ease of reading):
{
"businesses": [
{
"id": "fob-poke-bar-seattle-2",
"name": "FOB Poke Bar",
"is_closed": false,
"review_count": 421,
"categories": [
{
"alias": "poke",
"title": "Poke"
},
{
"alias": "salad",
"title": "Salad"
},
{
"alias": "hawaiian",
"title": "Hawaiian"
}
],
"rating": 5,
"coordinates": {
"latitude": 47.6138005187095,
"longitude": -122.343868017197
},
"price": "$$",
"location": {
"city": "Seattle",
"zip_code": "98121",
"country": "US",
"state": "WA",
"display_address": [
"220 Blanchard St",
"Seattle, WA 98121"
]
},
}
Here it is in JSON Viewer
I access first level properties like name easily, and I access lattitude and longitude like so:
class Business: Codable {
var name: String
var rating: Double
var coordinates: Coordinates
struct Coordinates: Codable {
var latitude: Double
var longitude: Double
init(lat: Double, long: Double) {
self.latitude = lat
self.longitude = long
}
}
init(name: String, rating: Double, coordinates: Coordinates) {
self.name = name
self.rating = rating
self.coordinates = coordinates
self.categories = categories
}
}
Could anyone please point me in the right direction towards accessing categories -> title? Coordinates was easy to access but categories is an array of dictionaries. Thank you!
It's the same pattern like Coordinates except the value for categories is an array:
struct Root : Decodable {
let businesses : [Business]
}
struct Business: Decodable {
let name: String
let rating: Int
let coordinates: Coordinates
let categories : [Category]
struct Coordinates: Codable {
let latitude: Double
let longitude: Double
}
struct Category: Codable {
let alias: String
let title: String
}
}
let root = try decoder.decode(Root.self, from: data)
for business in root.businesses {
for category in business.categories {
print(category.title)
}
}

Resources