Serializing JSON ReadingOptions with inconsistent output in Swift 4.2 - swift

I'm working on a pet project where I am serializing JSON using the JSONSerialization class and jsonObject(with:options:). The object is unusable until cast into a Dictionary [String: Any] or an Array [Any]. This is where the inconsistency occurs. The following is a method from one of my classes. The input is tested and valid.
private static func parse(data: Data) -> [JSONDictionary]? {
do {
let options = JSONSerialization.ReadingOptions() // rawValue = UInt 0
let otherOptions: JSONSerialization.ReadingOptions = [] // rawValue = UInt 0
let jsonAny = try JSONSerialization.jsonObject(with: data, options: otherOptions)
if let array = jsonAny as? [String: Any] {
print(array)
}
} catch {
return nil
}
return nil
}
Both of the ReadingOption objects are valid and produce valid output that can be properly cast, and print(array) is called.
However, when I use the following, invalid output is returned and can not be cast properly. Note options in the jsonObject call has an equivalent value to otherOptions in the above example.
private static func parse(data: Data) -> [JSONDictionary]? {
do {
let jsonAny = try JSONSerialization.jsonObject(with: data, options: [])
if let array = jsonAny as? [String: Any] {
print(array) // never called
}
} catch {
return nil
}
return nil
}
I thought because they have equivalent values that I could use them in place of each other. But that is not the case. Is this a bug, or am I using this incorrectly?
Edit: here is the dataset being used https://www.govtrack.us/api/v2/role?current=true&role_type=senator

The reading options are irrelevant. In Swift ReadingOptions are only useful if the expected result is not array or dictionary.
If the expected type is array or dictionary omit the options parameter.
The inconsistency is that your return type is an array ([JSONDictionary]) but the actual type is a dictionary.
private static func parse(data: Data) -> JSONDictionary? {
do {
let jsonAny = try JSONSerialization.jsonObject(with: data)
if let jsonDictionary = jsonAny as? JSONDictionary {
return jsonDictionary
}
} catch {
print(error)
return nil
}
return nil
}
It's recommended to hand over an error of a throwing method
enum SerializationError : Error {
case typeMismatch
}
private static func parse(data: Data) throws -> JSONDictionary {
let jsonAny = try JSONSerialization.jsonObject(with: data)
guard let jsonDictionary = jsonAny as? JSONDictionary else { throw SerializationError.typeMismatch }
return jsonDictionary
}

Related

Decodable extension not called Xcode

I am trying to decode the array IGNotification into data that was successfully encoded into a dictionary. Below is my compactMap that should call my Decodable extension, however it looks like my extension is not being called or that the dictionary is not being serialized. Can someone help me as to why this is?
getNotifications function:
public func getNotifications(
completion: #escaping ([IGNotification]) -> Void
) {
guard let username = UserDefaults.standard.string(forKey: "username") else {
completion([])
return
}
let ref = firestoreDatabase.collection("users").document(username).collection("notifications")
ref.getDocuments { snapshot, error in
guard let notifications = snapshot?.documents.compactMap({
IGNotification(with: $0.data())
}),
error == nil else {
completion([])
return
}
completion(notifications)
print(notifications)
}
}
Decodable extension:
extension Decodable {
/// Create model with dictionary
/// - Parameter dictionary: Firestore data
init?(with dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(
withJSONObject: dictionary,
options: .prettyPrinted)
else {
return nil
}
guard let result = try? JSONDecoder().decode(
Self.self,
from: data
) else {
return nil
}
self = result
}
}

Dictionary extension in Swift is not recognized

I am trying to extend Dictionary with the following code:
extension Dictionary where Key: ExpressibleByStringLiteral, Value: AnyObject {
var jsonString: String? {
if let dict = (self as AnyObject) as? Dictionary<String, AnyObject> {
do {
let data = try JSONSerialization.data(withJSONObject: dict, options: JSONSerialization.WritingOptions(rawValue: UInt.allZeros))
if let string = String(data: data, encoding: String.Encoding.utf8) {
return string
}
} catch {
print(error)
}
}
return nil
}
}
Where I write something like:
let x: [String: String] = ["": ""]
x.jsonString
I get this error:
Value of type '[String: String]' as no member 'jsonString'
Anything I am missing?
There is no need to constrain Dictionary Value type at all:
extension Dictionary where Key: ExpressibleByStringLiteral {
var jsonString: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self)
else { return nil }
return String(data: data, encoding: .utf8)
}
}
Since String is a value type , look for it's
public struct String {
and AnyObject refers to any instance of a class only , and is equivalent to id in Objective-C , so this declaration of x
[String: String] doesn't fit with [String: AnyObject]
because Any refers to any instance of a class, struct, or enum so it'll fit perfectly

Codable to CKRecord

I have several codable structs and I'd like to create a universal protocol to code them to CKRecord for CloudKit and decode back.
I have an extension for Encodable to create a dictionary:
extension Encodable {
var dictionary: [String: Any] {
return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self), options: .allowFragments)) as? [String: Any] ?? [:]
}
}
Then in a protocol extension, I create the record as a property and I try to create a CKAsset if the type is Data.
var ckEncoded: CKRecord? {
// Convert self.id to CKRecord.name (CKRecordID)
guard let idString = self.id?.uuidString else { return nil }
let record = CKRecord(recordType: Self.entityType.rawValue,
recordID: CKRecordID(recordName: idString))
self.dictionary.forEach {
if let data = $0.value as? Data {
if let asset: CKAsset = try? ckAsset(from: data, id: idString) { record[$0.key] = asset }
} else {
record[$0.key] = $0.value as? CKRecordValue
}
}
return record
}
To decode:
func decode(_ ckRecord: CKRecord) throws {
let keyIntersection = Set(self.dtoEncoded.dictionary.keys).intersection(ckRecord.allKeys())
var dictionary: [String: Any?] = [:]
keyIntersection.forEach {
if let asset = ckRecord[$0] as? CKAsset {
dictionary[$0] = try? self.data(from: asset)
} else {
dictionary[$0] = ckRecord[$0]
}
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { throw Errors.LocalData.isCorrupted }
guard let dto = try? JSONDecoder().decode(self.DTO, from: data) else { throw Errors.LocalData.isCorrupted }
do { try decode(dto) }
catch { throw error }
}
Everything works forth and back except the Data type. It can't be recognized from the dictionary. So, I can't convert it to CKAsset. Thank you in advance.
I have also found there is no clean support for this by Apple so far.
My solution has been to manually encode/decode: On my Codable subclass I added two methods:
/// Returns CKRecord
func ckRecord() -> CKRecord {
let record = CKRecord(recordType: "MyClassType")
record["title"] = title as CKRecordValue
record["color"] = color as CKRecordValue
return record
}
init(withRecord record: CKRecord) {
title = record["title"] as? String ?? ""
color = record["color"] as? String ?? kDefaultColor
}
Another solution for more complex cases is use some 3rd party lib, one I came across was: https://github.com/insidegui/CloudKitCodable
So I had this problem as well, and wasn't happy with any of the solutions. Then I found this, its somewhat helpful, doesn't handle partial decodes very well though https://github.com/ggirotto/NestedCloudkitCodable

Cannot subscript a value of type 'AnyObject' with an index of type 'String'

i tried to get The (Name) value to a label
i used resultLabel.text = !(jsonResult["name"]) but it returns an error
Cannot subscript a value of type 'AnyObject' with an index of type 'String'
See my JSON
and does anybody know's how to get the data..
My code ....
if let url = URL(string: "http://www.omdbapi.com/?t=The+Raid&y=&plot=short&r=json") {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in // URLSession.shared().dataTask(with: url) { (data, response, error) is now URLSession.shared.dataTask(with: url) { (data, response, error)
if error != nil {
print(error)
} else {
if let urlContent = data {
do {
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject // Added "as anyObject" to fix syntax error in Xcode 8 Beta 6
print(jsonResult)
print(jsonResult["Title"])
resultLabel.text = (jsonResult["name"])
if let description = ((jsonResult["weather"] as? NSArray)?[0] as? NSDictionary)?["description"] as? String {
DispatchQueue.main.sync(execute: {
self.resultLabel.text = description
})
}
} catch {
print("JSON Processing Failed")
}
}
}
}
task.resume()
} else {
resultLabel.text = "Couldn't find weather for that city - please try another."
}
}
Casting the result of JSON deserialization to AnyObject is the worst you can do.
First of all the unspecified JSON type is Any and since the type is supposed to be a dictionary, cast it to [String:Any].
Further in Swift 3 the compiler must know all types which are subscripted
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: []) as! [String:Any]
let name = jsonResult["name"] as? String
print(name ?? "n/a")
if let weather = jsonResult["weather"] as? [[String:Any]], !weather.isEmpty {
if let description = weather[0]["description"] as? String {
DispatchQueue.main.async { // not sync !!
self.resultLabel.text = description
}
}
...
PS: As always, mutableContainers is meaningless in Swift and meaningless anyway if the values are only read.

Convert JSON String to Swift Dictionary

I'm trying to convert a json string to a dictionary.
I came across this answer but it only works for arrays with one value.
Here is an example of a string I'm trying to turn into a swift dictionary
[
{"value1":"Reporting for duty"},
{"value2":"Post received"},
{"value3":"frogg222"},
{"value4":"Still reporting"}
]
Just to be clear, the following code does not work and will return nil with the above string.
func convertStringToDictionary(text: String) -> [String:AnyObject]? {
if let data = text.data(using: String.Encoding.utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String:AnyObject]
} catch let error as NSError {
print(error)
}
}
return nil
}