I am writing a basic JSON parser, facing this strange issue where json loaded from file is different from the one that is hard coded.
This is the content of json file that I am using
{
"details": {
"date": "2019-02-08T11:08:38Z",
"busId": 4,
"end_date": {
"date": "2019-02-13T18:30:00Z",
"flex": 0,
"timezone": "Asia/Calcutta",
"hasTime": false,
"userDate": "2019-02-14T00:00:00Z"
}
}
}
and the code to load the json in Swift is
func jsonFromFile(_ name: String) -> [String : Any] {
let path = Bundle.main.path(forResource: name, ofType: "json")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
let jsonObj = try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
return jsonObj
}
This is how I created a literal JSON in Swift
let data: [String : Any] = [
"details": [
"date": "2019-02-08T11:08:38Z",
"busId": 4,
"end_date": [
"date": "2019-02-13T18:30:00Z",
"flex": 0,
"timezone": "Asia/Calcutta",
"hasTime": false,
"userDate": "2019-02-14T00:00:00Z"
]
]
]
I was facing a strange issue while parsing it. After digging I found out that the internal representation of Dictionary created from JSON in file is different from the JSON created from dictionary literal.
Here is the debug output when loaded from file
and when created from literal Swift dictionary
Although they are of type [String : Any] they have different internal representation(note the curly brackets in first image).
One of the problem I think could be JSONSerialization.jsonObject would be returning an object of type NSDictionary instead of [String : Any]; although they are bridged but they could have different internal implementation.
So,
How do I make sure that I get a same representation of JSON in [String : Any] irrespective of where it is loaded from.
Update:
I tried using type(of:) in debugger. Type of first case is __NSDictionaryI and other one is Swift.Dictionary<Swift.String, Any>, so clearly the internal type is different. Do I straight away look for solutions that would let me convert NSDictionary to Swift.Dictionary?
Note: The reason behind asking this question is, although they behave same in most cases, I am facing some issue while converting the variable to protocol type. It works fine for Swift.Dictionary, but the code breaks for NSDictionary(i.e. object loaded from JSON file)
Here is the protocol
protocol Countable {
var count: Int { get }
}
extension Array: Countable where Element: Any {
}
extension Dictionary: Countable where Key == String, Value == Any {
}
So for the same variable when I write if var1 is Countable it returns true for first case and false for second case. Although it works fine if I write separate type check for [Any] and [String: Any]
Don't worry, they have the same internal structure and you can use them vice versa. The only difference is class or structure they use for storage (debugger shows you output of description method. So, curly brackets are product of this description method, not more.).
You can try to compare them in code with isEqual(to:) method and you will see that they are equal. It compares internal structure and content of collections.
Related
I'm a beginner in swift and I'm currently making an app that makes a web request. I've been trying to parse this JSON Data but the nested data is just really hard to wrap my head around:
"abilities": [
{
"ability": {
"name": "chlorophyll",
"url": "https://pokeapi.co/api/v2/ability/34/"
},
"is_hidden": true,
"slot": 3
},
{
"ability": {
"name": "overgrow",
"url": "https://pokeapi.co/api/v2/ability/65/"
},
"is_hidden": false,
"slot": 1
}
]
JSon Serialization Code
let jsonAny = try JSONSerialization.jsonObject(with: data, options: [])
guard let json = jsonAny as? [String: Any] else { return }
This is my attempt to manually parse the JSON Data
private func parsePokemonManual(json: [String: Any]) -> Pokemon {
let abilities = json["abilities"] as? [String: Any] ?? [String: Any]()
return Pokemon(abilities: abilities)
}
}
These are the structs that I made to hold the data.
struct Abilities {
let ability : Ability
struct Ability {
let name : String
}
}
How do I successfully parse the JSON Data into an object of Pokemon structure?
With this code so fat I am getting the error "Cannot convert the value of type '[String : Any]' to expected argument type '[Abilities]'. My problem is that I don't know what type to cast the abilities as and that my struct 'Abilities' is also incorrect.
There are 3 problems with your attempt although one might argue there is only 1, that you should use Codable instead but lets stay with JSONSerialization here. The problems are
You are reading the json wrong and should cast not to a dictionary but an array of dictionaries when accessing "abilities"
Your struct is to complicated, maybe because of the previous problem
Lastly, you can't cast into a custom type, you need to convert or map the data into your type by telling exactly what values to use and how because the compiler doesn't understand how to do it.
First the struct can be simplified to
struct Ability {
let name : String
}
And the rest is fixed in the function parsePokemonManual. First get "abilities" and cast to an array of dictionaries. Then map each item in the array by getting "ability" and casting it to a dictionary that is used to get the "name" value that is then used when creating an instance of Ability
private func parsePokemonManual(json: [String: Any]) -> [Ability] {
guard let abilities = json["abilities"] as? [[String: Any]] else {
return []
}
return abilities.compactMap { dict in
guard let ability = dict["ability"] as? [String: String], let name = ability["name"] else { return nil }
return Ability(name: name)
}
}
I want to combine 3 json results into one for looping into later.
I have tried every single combination on the internet yet I'm still stuck a dead end. I am calling an API call that in response, receives json data. I want to get the first 3 results and return that data to the calling method however, I just cannot find a way to find the desired response.
var data: JSON = []
func getData(forKey url: URL, completion: #escaping ([JSON]) -> Void){
AF.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
for loopID in 0 ... 2{
print(swiftyJsonVar["results"][loopID])
}
}
return completion([self.data])
}
}
My desired results are
[
{
"name": "james",
"location": "Mars"
},
{
"name": "james",
"location": "Mars"
},
{
"name": "james",
"location": "Mars"
}
]
When, in my loop, I receive, x 3
{
"name": "james",
"location": "Mars"
}
For starters, I'd put in a breakpoint and see what your response object looks like from whatever endpoint you're hitting. Perhaps those really are the first three elements of the array?
Moving along to the matter at hand, I would use the Codable protocol for decoding your response. I'll skip the Alamofire part and focus solely on decoding the object. Using your example, here's what the struct would look like:
struct ResponseObject: Codable {
let name: String
let location: String
}
I would avoid using the bang operator (!) in your code, as you'll throw an exception if you tell the compiler it's 100% guaranteed money in the bank the object's not nil and it actually is. Instead, unwrap optionals.
You'll need a landing spot for your decoded data, so you could declare an empty array of responses, as follows:
var arrayOfObjects: [ResponseObject] = []
Then, you'd just declare a decoder and decode your data:
let decoder = JSONDecoder()
do {
if let data = rawData {
arrayOfObjects = try decoder.decode([ResponseObject].self, from: data)
print("number of objects:", arrayOfObjects.count)
let slice = arrayOfObjects.prefix(3)
arrayOfObjects = Array(slice)
print("number of objects after slicing the array:", arrayOfObjects.count)
}
} catch {
print(error)
}
Instead of looping through the array, just grab the first three elements of the array with .prefix(3). Fiddling with this just now, I tried prefixing the first 10 elements of an array with 4 and it didn't generate an exception, so I think that should work without checking the count of the array first.
I would suggest taking a look through Swift documentation online re: Arrays or you could get a pretty sweet Mac OS app called Dash that lets you load up docsets for a bunch of languages on your machine locally.
Good luck!
According to the JSON standard RFC 7159, this is valid json:
22
How do I decode this into an Int using swift4's decodable? This does not work
let twentyTwo = try? JSONDecoder().decode(Int.self, from: "22".data(using: .utf8)!)
It works with good ol' JSONSerialization and the .allowFragments
reading option. From the documentation:
allowFragments
Specifies that the parser should allow top-level objects that are not an instance of NSArray or NSDictionary.
Example:
let json = "22".data(using: .utf8)!
if let value = (try? JSONSerialization.jsonObject(with: json, options: .allowFragments)) as? Int {
print(value) // 22
}
However, JSONDecoder has no such option and does not accept top-level
objects which are not arrays or dictionaries. One can see in the
source code that the decode() method calls
JSONSerialization.jsonObject() without any option:
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
// ...
return value
}
In iOS 13.1+ and macOS 10.15.1+ JSONDecoder can handle primitive types on root level.
See the latest comments (Oct 2019) in the linked article underneath Martin's answer.
I have a function in Swift that needs to be able to handle multiple types. Specifically, it needs to be able to parse both Dicts and Strings.
The problem I have is the Dicts could be several types, depending on their origin. So I could be provided with [String:Any] or [String:String] (coming from Swift) or [String:AnyObject] (coming from objc). The top level parsing function takes Any, which it then tests for specific types and attempts to parse them.
At first I just tried testing for if let dict = object as? [String:Any], but if I passed in another type [String:AnyObject] or [String:String] it failed. So I tried testing each type:
func parseLink(object: Any) {
if let dict = object as? [String:Any] {
return self.parseDict(dict)
} else if let dict = object as? [String:AnyObject] {
return self.parseDict(dict)
} else if let dict = object as? [String:String] {
return self.parseDict(dict)
} else if let string = object as? String {
return parseURL(string)
}
}
func parseDict(dict: [String:Any]) { ..... }
So I've created some Unit Tests to test the behavior:
func testDictTypes() {
let testDict: [String:Any] = [ "orgId" : "123456789" ]
let link = OrgContextLinkParser().parseLink(testDict)
XCTAssertNotNil(link)
let testDict1: [String:AnyObject] = [ "orgId" : "123456789" ]
let link2 = OrgContextLinkParser().parseLink(testDict1)
XCTAssertNotNil(link2)
let testDict3: [String:String] = [ "orgId" : "123456789" ]
let link3 = OrgContextLinkParser().parseLink(testDict3)
XCTAssertNotNil(link3)
}
This all compiles fine, but I get a fatal runtime error if a [String:AnyObject] is passed in. This is troubling since Swift's type system is supposed to prevent these kind of errors and I get no warning or errors thrown when I compile.
I also really don't want to duplicate the exact same logic multiple times just to handle different dict types. I.E., handling [String:Any], [String:AnyObject] and [String:String] have virtually the exact same logic.
The only possible solution I've seen is to actually duplicate the dictionary, which seems rather expensive (Convert [String: AnyObject] to [String: Any]). For performance reasons, it seems better to just copy paste the code and change the function signatures... but really!? That's seems excessive.
The best solution seems to be to parse a Dict as [String:AnyObject] and copy the value only if it's [String:Any]:
if let dict = object as? [String:Any] {
var objDict: [String:AnyObject] = [:]
for (key, value) in dict {
if let obj = value as? AnyObject {
objDict[key] = obj
}
}
return self.parseDict(objDict)
I don't particularly like this, but so far it's be best I've been able to come up with.
Does anyone have any idea how to handle this properly? I'm especially concerned that I can cast Any as [String:AnyObject], pass it to a function that takes [String:Any] and I get no compiler errors, even though it crashes at runtime.
I have a dictionary which i convert to a string to store it in a database.
var Dictionary =
[
"Example 1" : "1",
"Example 2" : "2",
"Example 3" : "3"
]
And i use the
Dictionary.description
to get the string.
I can store this in a database perfectly but when i read it back, obviously its a string.
"[Example 2: 2, Example 3: 3, Example 1: 1]"
I want to convert it back to i can assess it like
Dictionary["Example 2"]
How do i go about doing that?
Thanks
What the description text is isn't guaranteed to be stable across SDK versions so I wouldn't rely on it.
Your best bet is to use JSON as the intermediate format with NSJSONSerialization. Convert from dictionary to JSON string and back.
I created a static function in a string helper class which you can then call.
static func convertStringToDictionary(json: String) -> [String: AnyObject]? {
if let data = json.dataUsingEncoding(NSUTF8StringEncoding) {
var error: NSError?
let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &error) as? [String: AnyObject]
if let error = error {
println(error)
}
return json
}
return nil
}
Then you can call it like this
if let dict = StringHelper.convertStringToDictionary(string) {
//do something with dict
}
this is exactly what I am doing right now. Considering #gregheo saying "description.text is not guaranteed to be stable across SKD version" description.text could change in format-writing so its not very wise to rely on.
I believe this is the standard of doing it
let data = your dictionary
let thisJSON = try NSJSONSerialization.dataWithJSONObject(data, options: .PrettyPrinted)
let datastring:String = String(data: thisJSON, encoding: NSUTF8StringEncoding)!
you can save the datastring to coredata.