{"JSON, Swift and Type Safety" : "It's a wrap"}
@gylphi @sketchytech
[Anthony Levings,@sketchyTech]
Presented at SwiftSummit.com, 21 March 2015
NSDataNSDataif letNSData?NSData?
AnyObject?AnyObject?NSJSONSerializationNSDataNSData
NSArrayNSArray
AnyObject?AnyObject?
NSDictionaryNSDictionary
AnyObjectAnyObject AnyObjectAnyObject AnyObjectAnyObject
NSArrayNSArray
NSArrayNSArrayNSDictionaryNSDictionary
NSStringNSString
AnyObjectAnyObjectNSNumberNSNumber NSNullNSNull
Safety Last“Smash and Grab”
var error:NSError?
if let jsonObject = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &error) as? [String: AnyObject] {
let count = jsonObject["resultCount"] as? Int // casting value to Int
}
var error:NSError?
if let jsonObject = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &error) as? [String: AnyObject] {
var jDict = jsonObject jDict["resultCount"] = "string"// change of type can happen easily
let jsonData = NSJSONSerialization.dataWithJSONObject(jDict, options: nil, error: nil)
}
Simple Safety“Dream Data”
if the JSON you are receiving looks like this
{"key1":"value1","key2":"value2","key3":"value3"}
or this
[1,2,3,4,5,6,6,7,8,9,10]
then you can simply write
if let dict = jsonObject as? Dictionary<String,String> { }
or this
if let dict = jsonObject as? [Int] { }
to achieve type safety, but this will rarely the case in the real world and so rather than keep dreaming, we
have…
Safely Wrappedenum with associated values
enum Value {
// enum cases case StringType(String) case NumberType(NSNumber) case NullType(NSNull) // collection types case DictionaryType(Dictionary<String,Value>) case ArrayType([Value]) }
And we then wrap each value of a received [AnyObject] or [String: AnyObject] as the
initializer to our enum*
* working code available, ask me after if you’re interested
if let num = dictionary["key"]?.number { }
RATHER THAN THIS:
if let dict = jsonObj as? [String: AnyObject],str = dict[“key”] as? NSNumber { }
We can then combine associated values with computed variables to achieve this kind of syntax:
Argo (thoughtbot)
Swiftz (typelift)
json-swift (David Owens II)
Three GitHub Swift–JSON libraries that already use associated value enums in their code:
Type Safety = Empowerment
• restrict changes of type (e.g. through subscripting)• prevent the return of AnyObject• enable the compiler to better detect errors and assist the
programmer• reduction in the amount of code to test types and return
values• IT MAKES US THINK ABOUT TREATMENT OF JSON!
Potential Problems
The larger your model object, the longer the build takes [using Argo]. This is an issue with the Swift compiler having trouble working out all the nested type inference. While Argo works, it can be impracticle for large objects. There is work being done on a separate branch to reduce this time. (Tony DiPasquale, thoughtbot)
https://robots.thoughtbot.com/parsing-embedded-json-and-arrays-in-swift
Argo
Wrapped on DemandA possible solution
enum Value {
// enum cases case StringType(String) case NumberType(NSNumber) case NullType(NSNull) // collection types case DictionaryType(JSONDictionary) case ArrayType(JSONArray) }
If we use a struct and an enum together we can leverage stored values:
(1) the getter can wrap individual values on demand (not in advance).
(2) changes and additions to stored values become simplified
if let p = parsedJSON["results"]?.jsonArr, d = p[0]?.jsonDict { d["trackName"]?.str }
parsedJSON["results"]?[0]?["trackName"] = "Something"
And setting:
Getting:
Using the struct approach we also have easier access to information like which keys have String values, which have Number values, etc.
—- Dictionary —-json.keysWithNumberValuesjson.keysWithStringValues
—- Array ——-json.isNumberArrayjson.isStringArrayjson.isMixedArray
json.removeAllNumbers()json.removeAllStrings()
and other benefits of stored properties, which enums don’t enjoy.
Bespoke Handling of Data
if let url = NSURL(string:"http://itunes.apple.com/search?term=b12&limit=40"), data = NSData(contentsOfURL: url), parsedJSON = JSONParser.parseDictionary(data), iTD = iTunesData(dict: parsedJSON){ let tracks = map(iTD.results, {x in Track(dict:x.jsonDict)})}
Bespoke Handling of Data
public struct iTunesData { public var resultCount:Int { return results.count } public var results:JSONArray public init?(dict:JSONDictionary) { ... } public subscript (index:Int) -> JSONDictionary? { ... } public mutating func updateTrackDetails(track:Track) { ... } public func outputJSON() -> NSData? { ... }}
public struct Track { public var trackName:String, collectionName:String, trackId:Int public init?(dict:JSONDictionary?) { if let tN = dict?["trackName"]?.str, cN = dict?["collectionName"]?.str, tI = dict?["trackId"]?.num { trackName = tN collectionName = cN trackId = tI.integerValue } else { return nil } }}
Round-tripping bespoke data
Track info
iTunesData
JSONParser
JSON in
JSON out
JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999. JSON is a text format that is completely language independent but uses conventions that are familiar to programmers of the C-family of languages, including C, C++, C#, Java, JavaScript, Perl, Python, and many others. These properties make JSON an ideal data-interchange language.