How to parse array of objects of json with SwiftUI - json

completely new to Swift here, so I have no idea how to even use print debugging, so thought I would ask here.
I am trying to parse a series of objects, each one of them in this format:
{
"id": 0,
"title": "Example Title",
"targets": [
{
"name": "Example Name"
},
{
"name": "Example Name"
},
{
"name": "Example Name"
},
{
"name": "Example Name"
}
],
"benefits": [
{
"name": "Example Benefit"
},
{
"name": "Example Benefit"
},
{
"name": "Example Benefit"
}
],
"steps": [
{
"name": "Example Step"
},
{
"name": "Example Step"
}
],
"videoURL": "https://someurl.com"
},
So I have a struct defined as such
struct Obj: Codable, Hashable {
var id:Int
var title:String
var targets:[String]
var benefits:[String]
var steps:[String]
var videoURL:String
}
And using
let objs:[Obj] = decode([Obj].self, from: "./Data/Objs.json")
Where decode is this function
func decode<T: Decodable>(_ type:T.Type, from filename:String) -> T {
guard let json = Bundle.main.url(forResource: filename, withExtension: nil) else {
fatalError("Failed to locate \(filename) in app bundle.")
}
guard let jsonData = try? Data(contentsOf: json) else {
fatalError("Failed to load \(filename) from app bundle.")
}
let decoder = JSONDecoder()
guard let result = try? decoder.decode(T.self, from: jsonData) else {
fatalError("Failed to decode \(filename) from app bundle.")
}
return result
}
However when accessing the data in Objs, I get an error in the SwiftUI preview objs.app may have crashed. Check ... for any crash logs in your application which leads me to believe I am using JSONDecoder() incorrectly. Any help is appreciated!

So the problem here is how you are decoding inner content of the targets, steps and benifits.
Targets/Steps/Benifit contains array of object but in the Codable structure of yours, you have given it as a array of strings [String]. You need to correct that and Check.
Create one new structure:
struct Name : Codable {
var name : String
}
Your final object will look like:
struct Obj: Codable, Hashable {
var id:Int
var title:String
var targets:[Name]
var benefits:[Name]
var steps:[Name]
var videoURL:String
}
The key thing you should remember is that at the time of parsing nested object you will have to create another structure with codable conformance.

Related

For Loop with json decode data error requires 'People' to conform to 'Sequence'

I am new to swift . I created simple playground and added the file with extension json into playground . I am trying to decode the result and print the ID by using for loop but , I am getting following error ..
For-in loop requires 'People.Type' to conform to 'Sequence'
Here is my json file ..
{
"id": "1",
"options": [{
"id": "11",
"options": [{
"id": "111",
"options": []
}]
},
{
"id": "2",
"options": [{
"id": "21",
"options": []
},
{
"id": "22",
"options": [{
"id": "221",
"options": []
}]
}
]
}
]
}
Here is the code .. I tried ..
struct People: Codable {
let id: String
let options: [People]
}
func loadJson(filename fileName: String) -> People? {
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(People.self, from: data)
print("// Printing the ID of the Decode Json")
for jsondata in jsonData {
print("ID: \(jsonData.id)")
}
return jsonData
} catch {
print("error:\(error)")
}
}
return nil
}
loadJson(filename: "people1")
Here is the screenshot of the error ..
Here I got to run exactly what you are asking for, however I think you should probably consider renaming your struct to something like Person as #Vadian suggested.
struct People: Codable {
let id: String
let options: [People]
}
func loadJson(filename fileName: String) -> People? {
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode(People.self, from: data)
printPeople(people: [jsonData])
return jsonData
} catch {
print("error:\(error)")
}
}
return nil
}
func printPeople(people: [People]) {
for person in people {
print(person.id)
if (!person.options.isEmpty) {
printPeople(people: person.options)
}
}
}
loadJson(filename: "people")
This will print:
1
11
111
2
21
22
221

JSON parsing error - No value associated with key CodingKeys

So, I have tried looking for the answer, I have seen many questions similar to mine. I have tried adding an enum codingKey, I have tried reworking the JSON, but nothing seems to work. I know it's probably something very simple too. (I'm a noob.) Please help.
I am making a Quotes app as part of a project for a course I'm taking.
Here's the code for the Model:
import Foundation
class AllQuotes: Identifiable, Decodable {
var id:UUID?
var quoteTopic:String
var topicImage:String
var featured:Bool
var QuotesList:[Quotes]
}
class Quotes: Identifiable, Decodable {
var id:UUID?
var name:String
var actualQuote:String
var image:String?
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case actualQuote = "actualQuote"
case image = "image"
}
}
Here's my JSON code:
[
{
"quoteTopic": "Wise Quotes",
"topicImage": "wise quotes",
"featured": false,
"QuotesList": [
{
"name": "Lao Tzu",
"actualQuote": "The journey of a thousand miles begins with one step.",
"image": null
},
{
"name": "Mark Twain",
"actualQuote": "It is better to keep your mouth closed and let people think you are a fool than to open it and remove all doubt.",
"image": null
},
{
"name": "Mark Twain",
"actualQuote": "The secret of getting ahead is getting started.",
"image": null
},
{
"name": "Babe Ruth",
"actualQuote": "It’s hard to beat a person who never gives up.",
"image": null
}
]
},
{
"quoteTopic": "Motivational Quotes",
"topicImage": "motivational quotes",
"featured": true,
"QuotesList": [
{
"name": "Mark Twain",
"actualQuote": "Age is an issue of mind over matter. If you don't mind, it doesn't matter.",
"image": null
},
{
"name": "Mahatma Gandhi",
"actualQuote": "Learn as if you will live forever, live like you will die tomorrow.",
"image": null
},
{
"name": "Mary Kay Ash",
"actualQuote": "Don’t limit yourself. Many people limit themselves to what they think they can do. You can go as far as your mind lets you. What you believe, remember, you can achieve.",
"image": null
},
{
"name": "Unknown",
"actualQuote": "Hold the vision, trust the process.",
"image": null
}
]
},
{
"quoteTopic": "Success Quotes",
"topicImage": "success quotes",
"featured": false,
"QuotesList": [
{
"name": "Estee Lauder",
"actualQuote": "I never dreamed about success. I worked for it.",
"image": null
},
{
"name": "Thomas Edison",
"actualQuote": "Opportunity is missed by most people because it is dressed in overalls and looks like work.",
"image": null
},
{
"name": "Tom Lehrer",
"actualQuote": "Life is like a sewer… what you get out of it depends on what you put into it.",
"image": null
},
{
"name": "Walt Disney",
"actualQuote": "All our dreams can come true, if we have the courage to pursue them.",
"image": null
}
]
}
]
and here's the error I'm getting
Couldn't decode json, try again (to get the actual quote)!
keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: "name", intValue: nil) ("name").", underlyingError: nil))
(NOTE: It RUNS/builds fine, but it just won't show the 'QuotesDetailView' page when I try to run it. It's SwiftUI.
Please let me know if I need to provide anymore information, thank you!
EDIT: here's how I decode my JSON
class DataService {
// Return an array of Quote Objects
static func getLocalData() -> [AllQuotes] {
// Begin the process of parsing the JSON File
// Get a URL path to json file
let pathString = Bundle.main.path(forResource: "quotes", ofType: "json")
// Check if pathString is nil, otherwise return empty Quotes List if it is.
guard pathString != nil else{
return [AllQuotes]()
}
// Create URL Object
let url = URL(fileURLWithPath: pathString!)
// Create Data Object
do {
let data = try Data(contentsOf: url)
// Parse the data
let decoder = JSONDecoder()
do {
let quoteData = try decoder.decode([AllQuotes].self, from: data)
// Set unique IDs for each instance
for newQuote in quoteData {
newQuote.id = UUID()
}
// Return the Quote
return quoteData
} catch {
// Couldn't decode json
print("Couldn't decode json, try again (to get the quotes TOPIC)!")
print(error)
}
} catch {
// Error fetching data from file
print("There was an error fetching the data from the file. - with the quote list!")
print(error)
}
// It didn't work, return an empty Quotes List
return [AllQuotes]()
}
// Return an array of ACTUAL Quotes Objects
static func getActualQuote() -> [Quotes] {
// Begin the process of parsing the JSON File
// Get a URL path to json file
let pathString = Bundle.main.path(forResource: "quotes", ofType: "json")
// Check if pathString is nil, otherwise return empty Quotes List if it is.
guard pathString != nil else{
return [Quotes]()
}
// Create URL Object
let url = URL(fileURLWithPath: pathString!)
// Create Data Object
do {
let data = try Data(contentsOf: url)
// Parse the data
let decoder = JSONDecoder()
do {
let actualQuoteData = try decoder.decode([Quotes].self, from: data)
// Set unique IDs for each instance
for actualQuote in actualQuoteData {
actualQuote.id = UUID()
}
// Return the Quote
return actualQuoteData
} catch {
// Couldn't decode json
print("Couldn't decode json, try again (to get the actual quote)!")
print(error)
}
} catch {
// Error fetching data from file
print("There was an error fetching the data from the file. - with the actual quote!")
print(error)
}
// It didn't work, return an empty Quotes List
return [Quotes]()
}
}
try this sample code, it shows how to decode your json data, and then display some info:
Since you don't show how you decode your json data, I'm gessing that
the error you get is due to decoding AllQuotes.self instead of [AllQuotes].self as required.
struct ContentView: View {
#State var quoteList = [Quotes]()
var body: some View {
List(quoteList) { quote in
Text(quote.name)
}
.onAppear {
let json = """
[
{
"quoteTopic": "Wise Quotes",
"topicImage": "wise quotes",
"featured": false,
"QuotesList": [
{
"name": "Lao Tzu",
"actualQuote": "The journey of a thousand miles begins with one step.",
"image": null
},
{
"name": "Mark Twain",
"actualQuote": "It is better to keep your mouth closed and let people think you are a fool than to open it and remove all doubt.",
"image": null
},
{
"name": "Mark Twain",
"actualQuote": "The secret of getting ahead is getting started.",
"image": null
},
{
"name": "Babe Ruth",
"actualQuote": "It’s hard to beat a person who never gives up.",
"image": null
}
]
},
{
"quoteTopic": "Motivational Quotes",
"topicImage": "motivational quotes",
"featured": true,
"QuotesList": [
{
"name": "Mark Twain",
"actualQuote": "Age is an issue of mind over matter. If you don't mind, it doesn't matter.",
"image": null
},
{
"name": "Mahatma Gandhi",
"actualQuote": "Learn as if you will live forever, live like you will die tomorrow.",
"image": null
},
{
"name": "Mary Kay Ash",
"actualQuote": "Don’t limit yourself. Many people limit themselves to what they think they can do. You can go as far as your mind lets you. What you believe, remember, you can achieve.",
"image": null
},
{
"name": "Unknown",
"actualQuote": "Hold the vision, trust the process.",
"image": null
}
]
},
{
"quoteTopic": "Success Quotes",
"topicImage": "success quotes",
"featured": false,
"QuotesList": [
{
"name": "Estee Lauder",
"actualQuote": "I never dreamed about success. I worked for it.",
"image": null
},
{
"name": "Thomas Edison",
"actualQuote": "Opportunity is missed by most people because it is dressed in overalls and looks like work.",
"image": null
},
{
"name": "Tom Lehrer",
"actualQuote": "Life is like a sewer… what you get out of it depends on what you put into it.",
"image": null
},
{
"name": "Walt Disney",
"actualQuote": "All our dreams can come true, if we have the courage to pursue them.",
"image": null
}
]
}
]
"""
if let data = json.data(using: .utf8) {
do {
let apiResponse = try JSONDecoder().decode([AllQuotes].self, from: data)
// print something
for quote in apiResponse {
print("---> quoteTopic: \(quote.quoteTopic)")
// all quotes
quoteList.append(contentsOf: quote.QuotesList)
}
} catch {
print("decode error: \(error)")
}
}
}
}
}
class AllQuotes: Identifiable, Decodable {
let id = UUID() // <-- here
var quoteTopic:String
var topicImage:String
var featured:Bool
var QuotesList:[Quotes]
// -- here
private enum CodingKeys: String, CodingKey {
// <-- here remove id
case quoteTopic, topicImage, featured, QuotesList
}
}
class Quotes: Identifiable, Decodable {
let id = UUID() // <-- here
var name:String
var actualQuote:String
var image:String?
// -- here
private enum CodingKeys: String, CodingKey {
// <-- here remove id
case name = "name"
case actualQuote = "actualQuote"
case image = "image"
}
}
EDIT-1: in view of the "new" code
struct ContentView: View {
#State var quoteList = [Quotes]()
var body: some View {
List(quoteList) { quote in
Text(quote.name)
}
.onAppear {
quoteList = DataService.getActualQuote()
print("---> quoteList: \(quoteList)")
}
}
}
class DataService {
// Return an array of Quote Objects
static func getLocalData() -> [AllQuotes] {
// Begin the process of parsing the JSON File
// Get a URL path to json file
let pathString = Bundle.main.path(forResource: "quotes", ofType: "json")
// Check if pathString is nil, otherwise return empty Quotes List if it is.
guard pathString != nil else{
return [AllQuotes]()
}
let url = URL(fileURLWithPath: pathString!)
do {
let data = try Data(contentsOf: url)
do {
let quoteData = try JSONDecoder().decode([AllQuotes].self, from: data)
return quoteData
} catch {
// Couldn't decode json
print("Couldn't decode json, try again (to get the quotes TOPIC)!")
print(error)
}
} catch {
// Error fetching data from file
print("There was an error fetching the data from the file. - with the quote list!")
print(error)
}
// It didn't work, return an empty Quotes List
return []
}
// Return an array of ACTUAL Quotes Objects
static func getActualQuote() -> [Quotes] {
// Begin the process of parsing the JSON File
// Get a URL path to json file
let pathString = Bundle.main.path(forResource: "quotes", ofType: "json")
// Check if pathString is nil, otherwise return empty Quotes List if it is.
guard pathString != nil else{
return [Quotes]()
}
// Create URL Object
let url = URL(fileURLWithPath: pathString!)
// Create Data Object
do {
let data = try Data(contentsOf: url)
do {
// -- here
let quoteData = try JSONDecoder().decode([AllQuotes].self, from: data)
// -- here
var actualQuoteData = [Quotes]()
for quote in quoteData {
actualQuoteData.append(contentsOf: quote.QuotesList)
}
// Return the Quotes
return actualQuoteData
} catch {
// Couldn't decode json
print("Couldn't decode json, try again (to get the actual quote)!")
print(error)
}
} catch {
// Error fetching data from file
print("There was an error fetching the data from the file. - with the actual quote!")
print(error)
}
// It didn't work, return an empty Quotes List
return []
}
}
EDIT-2: you can shorten your code, such as:
class DataService {
// Return an array of Quote Objects
static func getLocalData() -> [AllQuotes] {
if let pathString = Bundle.main.path(forResource: "quotes", ofType: "json") {
let url = URL(fileURLWithPath: pathString)
do {
let data = try Data(contentsOf: url)
let quoteData = try JSONDecoder().decode([AllQuotes].self, from: data)
return quoteData
} catch {
print(error)
}
}
return []
}
// Return an array of ACTUAL Quotes Objects
static func getActualQuote() -> [Quotes] {
var actualQuoteData = [Quotes]()
for quote in getLocalData() {
actualQuoteData.append(contentsOf: quote.QuotesList)
}
return actualQuoteData
}
}

Decoder not decoding json at keypath

Im trying to decode some JSON, but it's not parsing it. I think it may have something to to with either an incorrect KeyPath or the object itself. But I cannot figure it out.
This is the JSON that I want to decode (I want the array inside the docs path):
{
"status": 200,
"data": {
"docs": [
{
"_id": "60418a6ce349d03b9ae0669e",
"title": "Note title",
"date": "2015-03-25T00:00:00.000Z",
"body": "this is the body of my note.....",
"userEmail": "myemail#gmail.com"
}
],
"total": 1,
"limit": 20,
"page": 1,
"pages": 1
},
"message": "Notes succesfully Recieved"
}
Here's my decode function:
extension JSONDecoder {
func decode<T: Decodable>(_ type: T.Type, from data: Data, keyPath: String) throws -> T {
let toplevel = try JSONSerialization.jsonObject(with: data)
if let nestedJson = (toplevel as AnyObject).value(forKeyPath: keyPath) {
let nestedJsonData = try JSONSerialization.data(withJSONObject: nestedJson)
return try decode(type, from: nestedJsonData)
} else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Nested json not found for key path \"\(keyPath)\""))
}
}
}
And i'm calling it like this:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let notes = try decoder.decode([Note].self, from: data, keyPath: "data.docs")
Finally, this is my Note Struct:
struct Note: Codable {
var title: String?
let date: Date?
var body: String?
let userEmail: String?
}
The problem was that I was trying to decode date as a Date object instead of a String as is shown on the JSON.
Thanks #vadian!

How to parse local JSON data in Swift?

How to parse local JSON data where nested (optional) property is same as main.
Items data may be available or may not be available.
struct Category: Identifiable, Codable {
let id: Int
let name: String
let image: String
var items: [Category]?
}
I am using common Bundle extension to parse JSON data.
extension Bundle {
func decode<T: Codable>(_ file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "y-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
For eg data :
[
{
"id": 1,
"name": "Apple",
"image": "img_url",
"items" : [
{
"id": 1,
"name": "iPhone",
"image": "img_url",
"items" : [
{
"id": 1,
"name": "iPhone 11 Pro",
"image": "img_url"
},
{
"id": 2,
"name": "iPhone 11 Pro Max",
"image": "img_url"
}
]
},
{
"id": 2,
"name": "iPad",
"image": "img_url",
"items" : [
{
"id": 1,
"name": "iPad mini",
"image": "img_url"
},
{
"id": 2,
"name": "iPad Air",
"image": "img_url"
},
{
"id": 3,
"name": "iPad Pro",
"image": "img_url"
}
]
}
]
},
{
"id": 2,
"name": "Samsung",
"image": "img_url",
"items" : [
{
"id": 1,
"name": "Phone",
"image": "img_url"
},
{
"id": 2,
"name": "Tablet",
"image": "img_url"
}
]
}
]
Nesting is not the issue here, You are facing an Array of Contents. so you should pass [Content] to the decoder like:
let jsonDecoder = JSONDecoder()
try! jsonDecoder.decode([Category].self, from: json)
🎁 Property Wrapper
You can implement a simple property wrapper for loading and decoding all of your properties:
#propertyWrapper struct BundleFile<DataType: Decodable> {
let name: String
let type: String = "json"
let fileManager: FileManager = .default
let bundle: Bundle = .main
let decoder = JSONDecoder()
var wrappedValue: DataType {
guard let path = bundle.path(forResource: name, ofType: type) else { fatalError("Resource not found") }
guard let data = fileManager.contents(atPath: path) else { fatalError("File not loaded") }
return try! decoder.decode(DataType.self, from: data)
}
}
Now you can have any property that should be loaded from a file in a Bundle like:
#BundleFile(name: "MyFile")
var contents: [Content]
Note that since the property should be loaded from the bundle, I raised a FatalError. Because the only person should be responsible for these errors is the developer at the code time (not the run time).

Am I converting JSON data to a Swift variable the long way?

I am reading json data from a file and trying to move it into a NSDictionary and I am wondering if there is a more concise way to do it. The code shown below works fine. In my program the Astruct-type variable has more than 25 Bstruct-type variables. I have several files similar to the one I show. So it leads to a lot of cumbersome programming.
import UIKit
struct Bstruct {
var label = String()
var inputTypeStr = String()
var list = [String]()
}
struct Astruct {
// in real situation this has a over 25 variables
// limited to 3 for this question
var number = Bstruct()
var customer = Bstruct()
var location = Bstruct()
}
func loadJson(forFilename fileName: String) -> NSDictionary? {
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
if let data = NSData(contentsOf: url) {
do {
let dictionary = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as? NSDictionary
return dictionary
} catch {
print("Error!! Unable to parse \(fileName).json")
}
}
print("Error!! Unable to load \(fileName).json")
}
return nil
}
func fillAstruct (value: NSDictionary) -> Bstruct {
return Bstruct(label: value["label"] as! String, inputTypeStr: value["inputTypeStr"] as! String, list: value["list"] as! [String])
}
var aVar = Astruct()
let json = loadJson(forFilename: "document")! as NSDictionary
let d = json["aTemplate"]! as! NSDictionary
print("d = \(d)")
// load d into aVar, appears to be complex particularly if the variable has a large number of variables in aVar and the json file
for d1 in d {
let key = String(describing: d1.0)
let value = d1.1 as! NSDictionary
switch key {
case "number":
aVar.number = fillAstruct(value: value)
case "customer":
aVar.customer = fillAstruct(value: value)
case "location":
aVar.location = fillAstruct(value: value)
default: break
}
}
print("\(aVar)")
The JSON file is shown below:
{
"aTemplate": {
"number": {
"label": "Number",
"inputTypeStr": "selection",
"list": [
"A",
"B",
"C"
]
},
"customer": {
"label": "Customer",
"inputTypeStr": "label",
"list": [
""
]
},
"location": {
"label": "location",
"inputTypeStr": "label",
"list": [
""
]
}
}
}