Having trouble with print output of object decoded with JSONDecoder - ios

I'm trying to decode a JSON string in swift but having some weird issues accessing the properties once decoded.
This is the contents of the JSON file that I retrieve from a locally stored JSON file
[
{
"word": "a",
"usage": [
{
"partOfSpeech": "determiner"
}
]
}
]
And this is the code to access the properties of the JSON file
struct WordDictionary : Codable {
var word: String
var usage: [Usage]
}
struct Usage: Codable {
var partOfSpeech: String
}
if let url = Bundle.main.url(forResource: FILE_NAME, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let jsonData = try decoder.decode([WordDictionary].self, from: data)
print(jsonData[0].word) //Outputs "a"
print(jsonData[0].usage) //Outputs "[MyApp.AppDelegate.(unknown context at $102a37f00).(unknown context at $102a38038).Usage(partOfSpeech: "determiner")]"
} catch {
print("error:\(error)")
}
}
As you can see, when I try to print(jsonData[0].usage) I get a series of unknown data messages before I get the “Usage” property. When I print this line I just want to see determiner, I’m not sure what the preamble about the “unknown context” is all about.
I’m also running this code in didFinishLaunchingWithOptions function of the AppDelegate.
I’m not sure what I’m missing. I've been trying to find a solution for a few days now and trying different approaches but still can’t get the desired output, any help would be appreciated.

tl;dr
You are seeing the “unknown context” in the description of the type, because it was defined inside a function. You can solve this by either moving those type definitions outside of the function or implementing your own CustomStringConvertible conformance.
It's a matter of where you defined your types.
Consider:
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
struct WordDictionary: Codable {
var word: String
var usage: [Usage]
}
struct Usage: Codable {
var partOfSpeech: String
}
do {
let url = Bundle.main.url(forResource: "test", withExtension: "json")!
let data = try Data(contentsOf: url)
let words = try JSONDecoder().decode([WordDictionary].self, from: data)
print(words[0].usage)
} catch {
print(error)
}
return true
}
...
}
That produces:
[MyApp.AppDelegate.(unknown context at $102bac454).(unknown context at $102bac58c).Usage(partOfSpeech: "determiner")]
That is saying that Usage was defined in some unknown context within the AppDelegate within MyApp. In short, it does not know how to represent the hierarchy for types defined within functions.
Contrast that with:
class AppDelegate: UIResponder, UIApplicationDelegate {
struct WordDictionary: Codable {
var word: String
var usage: [Usage]
}
struct Usage: Codable {
var partOfSpeech: String
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
let url = Bundle.main.url(forResource: "test", withExtension: "json")!
let data = try Data(contentsOf: url)
let words = try JSONDecoder().decode([WordDictionary].self, from: data)
print(words[0].usage)
} catch {
print(error)
}
return true
}
...
}
Which produces:
[MyApp.AppDelegate.Usage(partOfSpeech: "determiner")]
You can also add your own CustomStringConvertible conformance:
struct WordDictionary: Codable {
var word: String
var usage: [Usage]
}
struct Usage: Codable {
var partOfSpeech: String
}
extension Usage: CustomStringConvertible {
var description: String { "Usage(partOfSpeech: \"\(partOfSpeech)\")" }
}
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
let url = Bundle.main.url(forResource: "test", withExtension: "json")!
let data = try Data(contentsOf: url)
let words = try JSONDecoder().decode([WordDictionary].self, from: data)
print(words[0].usage)
} catch {
print(error)
}
return true
}
...
}
Which produces:
[Usage(partOfSpeech: "determiner")]
Through CustomStringConvertible, you can make the print format it however you want.

If you want a type to print nicely when you interpolate an instance of it into a string, you need to make it conform to CustomStringConvertible.
This protocol declares one property: description and when string interpolation encounters an object that conforms to it, it uses the string returned by description instead.
You need something like:
extension Usage: CustomStringConvertible
{
var description: String
{
return "{ \"partOfSpeech\" : \"\(partOfSpeech)\" }"
}
}
if you want a JSON like* string to print.
An alternative would be to re-encode the object using a JSONEncoder and convert the data to a String. That's much heavier but might be a better option for more complex objects.
*JSON like because things like line feeds and tabs won't be replaced by escapes and " and \ appearing in the string won't be escaped.

Related

How can I get the document ID when using Codable in Firestore?

I want to get document id to work with it for editing and deleting document after decoding. How can I do this?
My model:
struct MoodRecord: Codable, Hashable, Identifiable {
#DocumentID var id: String?
let user: String
let date: String
let time: String
let mood: Int
}
My function:
class func getRecords <T: Decodable> (
reference: CollectionReference,
type: T.Type,
completion: #escaping (Result<[T], Error>) -> Void
) {
reference.whereField("user", isEqualTo: AuthManager.shared.getUserId() ?? "")
.getDocuments { snapshot, error in
if let documents = snapshot?.documents {
do {
let records: [T] = try documents.map { try $0.decoded(type: T.self) }
completion(.success(records))
} catch let error {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
}
My decoder:
extension QuerySnapshot {
func decoded <T: Decodable> (type: T.Type) throws -> [T] {
let objects: [T] = try documents.map { try $0.decoded(type: T.self) }
return objects
}
}
extension QueryDocumentSnapshot {
func decoded <T: Decodable> (type: T.Type) throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: data(), options: [])
let object = try JSONDecoder().decode(type.self, from: jsonData)
return object
}
}
I use only auto-ID in Firestore and want to work with them in this task. Can I do this?
You can use Firestore's Codable support to map document IDs. No need to implement a custom decoder - we've done all the hard work for you.
Here is how.
1. Create a model for your data
You already did this. Looking at the attributes in your MoodRecord struct, I assume you want to use date and time to track timestamps, and mood to capture the value of an enum. I've updated the struct accordingly:
struct MoodRecord: Codable, Hashable, Identifiable {
#DocumentID var id: String?
var user: String
var date: Date
var time: Date
var mood: Mood
}
enum Mood: String, Codable {
case great
case ok
case good
case bad
case terrible
}
2. Map data using Codable
Fetching Firestore documents and mapping them to Swift structs becomes a one-liner thanks to Codable:
docRef.getDocument(as: MoodRecord.self) { result in
// handle result
}
Here is a complete code snippet for fetching a single document:
private func fetchMoodRecord(documentId: String) {
let docRef = db.collection("moodrecords").document(documentId)
docRef.getDocument(as: MoodRecord.self) { result in
switch result {
case .success(let moodRecord):
// A MoodRecord value was successfully initialized from the DocumentSnapshot.
self.moodRecord = moodRecord
self.errorMessage = nil
case .failure(let error):
// A MoodRecord value could not be initialized from the DocumentSnapshot.
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
3. Updating a document
To update a document using Codable, use the following code snippet:
func updateMoodRecord(moodRecord: MoodRecord) {
if let id = moodRecord.id {
let docRef = db.collection("moodRecords").document(id)
do {
try docRef.setData(from: moodRecord)
}
catch {
print(error)
}
}
}
4.Adding new documents
Adding new documents is even easier:
func addMoodRecord(moodRecord: MoodRecord) {
let collectionRef = db.collection("moodRecords")
do {
let newDocReference = try collectionRef.addDocument(from: moodRecord)
print("Mood record stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
More
To learn more about how to map Firestore documents using Swift's Codable protocol, including how to map advanced data types such as date, time, colors, enums, how to fetch data using snapshot listeners, and how to handle any errors that might occur during the mapping process, check out Mapping Firestore Data in Swift - The Comprehensive Guide and the accompanying sample project on GitHub

attempt at parsing JSON file in Swift 5 ("Expected to decode Array<Any> but found a dictionary instead.")

I'm trying to make a stocks app for college related work and ive done almost everything i need to do except getting the actual data of the stocks into my app, I've been trying this and researching for the past couple days but still cannot get it to work as i get the error message :
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
any help is appreciated as this is the first ever thing i've made with swift/xcode
the code in my viewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchPostData { (posts) in
for post in posts {
print(post.datetime!)
}
}
}
func fetchPostData(completionHandler: #escaping ([Post]) -> Void) {
let url = URL(string: "https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=<api-key>")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let postsData = try JSONDecoder().decode([Post].self, from: data)
completionHandler(postsData)
}
catch {
let error = error
print(error)
}
}.resume()
}
}
and the other file with the variables:
struct Post: Codable {
var datetime: Int!
var open: Int!
var high: String!
var low: String!
var close: String!
var volume: String!
}
link to the json file: https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=0561a81a9baf4ae4bc65c7af9196f929
The error is speaking for itself; you are trying to decode an array but the JSON is a dictionary. You need to change your decode:
JSONDecoder().decode(Post.self, from: data)
Edit after Joakim Danielson hint for completeness of the answer:
You have also to modify your struct in order to accomodate the JSON in your response
struct Post: Decodable {
let meta: Meta
}
struct Meta: Decodable {
let symbol: String
let interval: String
let currency: String
let values: [Values]
// more data here
}
struct Values: Decodable {
// more data here
}
First of all, all values in Post are String, please note the double quotes in the JSON
struct Post: Decodable {
let datetime, open, high, low, close, volume: String
}
But the main issue is – as mentioned in the comments and Alastar's answer and indirectly stated by the error – you are ignoring the root object. You have to decode JSON always from the top. Add this struct
struct Root: Decodable {
let status: String
let values: [Post]
}
and decode
let postsData = try JSONDecoder().decode(Root.self, from: data)
completionHandler(postsData.values)

Swift: Loading Codable Class into CoreData from JSON is generating Objects with all properties to nil

Background:
I am creating a game with the option to buy/use power-ups. I want to pre-load these power-up objects into my CoreData database with a quantity of 0. The idea being that the user will buy power-ups and then the context of how many they own is saved in the database.
Problem:
My Codable objects are being generated with the properties all being nil or 0, i.e. not taking on the information provided by the JSON. Please can you help me see where I am going wrong.
My Codable conforming Class:
import Foundation
import CoreData
class PowerUp: NSManagedObject, Codable {
enum CodingKeys: String, CodingKey {
case name
case desc
case image
case quantity
}
var name: String?
var desc: String?
var image: String?
var quantity: Double?
required convenience init(from decoder: Decoder) throws {
guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.context,
let context = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName: "PowerUp", in: context) else {
fatalError("Failed to decode PowerUp")
}
self.init(entity: entity, insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
self.desc = try container.decodeIfPresent(String.self, forKey: .desc)
self.image = try container.decodeIfPresent(String.self, forKey: .image)
self.quantity = try container.decodeIfPresent(Double.self, forKey: .quantity)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.desc, forKey: .desc)
try container.encode(self.image, forKey: .image)
try container.encode(self.quantity, forKey: .quantity)
}
}
public extension CodingUserInfoKey {
// Helper property to retrieve the context
static let context = CodingUserInfoKey(rawValue: "context")
}
My JSON (powerUpData.json):
[
{
"name": "Extra Time",
"desc": "Wind back the clock with an extra 30 seconds.",
"image": "sand-clock",
"quantity": 0.0
},
{
"name": "Voice Trade",
"desc": "Offload an asset to a friend for 10% more than originally paid.",
"image": "microphone",
"quantity": 0.0
}
]
My AppDelegate (where the decoding and pre-loading is done):
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
preloadPowerUps()
return true
}
// Skipped out non-edited, irrelevant AppDelegate functions for clarity...
func preloadPowerUps() {
guard let url = Bundle.main.url(forResource: "powerUpData", withExtension: "json") else { fatalError("no file") }
do {
let json = try Data(contentsOf: url)
print(json)
let decoder = JSONDecoder()
decoder.userInfo[CodingUserInfoKey.context!] = persistentContainer.viewContext
do {
let subjects = try decoder.decode([PowerUp].self, from: json)
print(subjects)
do {
try persistentContainer.viewContext.save()
} catch {
print("error")
}
} catch {
print("error")
}
} catch {
print("error")
}
}
What's more is that when debugging, my PowerUp objects do seem to be taking on the values of my json but also kind of not...
To summarize from the questions comments:
It's important to remember that CoreData still relies heavily on Objective-C. In your example code, the properties on your class, although expressible as CoreData attributes, the implementation is not being handled by CoreData.
You'll need to add the #NSManaged attribute to your properties like this:
#NSManaged var name: String?
#NSManaged var desc: String?
#NSManaged var image: String?
#NSManaged var quantity: Double?
This will expose them to Obj-C as dynamic and allow CoreData to handle the implementation. This would also help to explain your debugging, in that at runtime the print statement would show values, but the saved managed object had nil values.

Swift Error saying `Instance member * cannot be used on type

I'm trying to read data from a json file from the bundle and to make this happen I created a protocol ReadableJSONFile and it has fileName to store the json file name and readFromJSONFile() to decode the file
public protocol ReadableJSONFile {
/// The name of your json file in the bundle
var fileName: String { get }
func readFromJSONFile() -> Self
}
And
public extension ReadableJSONFile where Self: Codable {
var fileName: String {
return "\(String(describing: Self.self)).json"
}
func readFromJSONFile() -> Self {
guard let url = Bundle.main.url(forResource: fileName, withExtension: nil) else {
fatalError("Failed to locate \(fileName) in app bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(fileName) in app bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(Self.self, from: data) else {
fatalError("Failed to decode \(fileName) from app bundle.")
}
return loaded
}
}
I wrote this simple json file and name it ModelA.json
{
"color": "yellow",
"name": "Mike"
}
And then created this model to represent the ModelA.json file
struct ModelA: Codable, ReadableJSONFile {
let color: String
let name: String
}
And inside my view controller I have this lines of code
class ViewController: UIViewController {
let data = ModelA.readFromJSONFile() // Here an error saying "Instance member 'readFromJSONFile' cannot be used on type 'ModelA'; did you mean to use a value of this type instead?"
override func viewDidLoad() {
super.viewDidLoad()
print(data.name)
}
}
When I make fileName and readFromJSONFile() as static everything works, but when I do that I can’t override the fileName inside ModelA as it’s confirmed to ReadableJSONFile
you can not access member of class that is not type members to do that you need to create object (instance) of your class like i have created below,
class ViewController: UIViewController {
let instance = ModelA()
let data = instance.readFromJSONFile()
override func viewDidLoad() {
super.viewDidLoad()
print(data.name)
}
}

parse JSON, iOS, Swift4

I am trying to parse some json but seem to be getting nil in the outputs.
I am not sure where I am going wrong and could use some help trying to figure this out.
struct albumInfo: Decodable {
var name: String?
var artist: String?
var url: String?
var playcount: String?
var listeners: String?
var releasedate: String?
var summary: String?
}
class SearchVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Choice = "album"
Album = "Believe"
Artist = "Cher"
let tryURL = "\(BASE_URL)\(Choice!).getinfo&api_key=\(API_KEY)&artist=\(Artist!)&album=\(Album!)&format=json"
print(tryURL)
guard let url = URL(string: tryURL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}
}.resume()
}
Here is the data as shown with the tryUrl.
First of all please conform to the naming convention that struct names start with a capital letter.
There are two major issues:
The root object is a dictionary with one key album containing the dictionary with keys name, listeners etc.
The key summary is in another dictionary for key wiki.
The structure of the JSON is very easy to identify. The body within each pair of braces ({}) represents a separate struct.
Further there is no key releasedate so this struct member has to be declared as optional, all other members can be declared as non-optional and as constants (let). url can be declared as URL for free.
Change your structs to
struct Root : Decodable {
let album : AlbumInfo
}
struct AlbumInfo: Decodable {
let name: String
let artist: String
let url: URL
let playcount: String
let listeners: String
let releasedate: String?
let wiki : Wiki
}
struct Wiki: Decodable {
let content: String
let published: String
let summary: String
}
and decode Root
let albumDescription = try JSONDecoder().decode(Root.self, from: data)
print(albumDescription.album.artist)
The first key of your response is "album", you need to parse that first.
The classes do not correspond to json, I guess you should use the following approach (new classes implement your decode, encode protocol):
class JsonInfo {
var album : albumInfo
}
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.album.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}

Resources