Best way to convert JSON or other untyped data into typed classes in Swift? - json

I'm trying to parse out JSON into typed classes for safety/convenience, but it's proving very clunky. I wasn't able to find a library or even a post for Swift (Jastor is as close as I got). Here's a fabricated little snippet to illustrate:
// From NSJSONSerialization or similar and casted to an appropriate toplevel type (e.g. Dictionary).
var parsedJson: Dictionary<String, AnyObject> = [ "int" : 1, "nested" : [ "bool" : true ] ]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(fromParsedJson json: NSDictionary) {
if let parsedStringValue = json["string"] as? String {
self.stringValueWithDefault = parsedStringValue
}
if let parsedIntValue = json["int"] as? Int {
self.intValueRequired = parsedIntValue
} else {
// Raise an exception...?
}
// Optional-chaining is actually pretty nice for this; it keeps the blocks from nesting absurdly.
if let parsedBool = json["nested"]?["bool"] as? Bool {
self.nestedBoolBroughtToTopLevel = parsedBool
}
if let parsedFirstInt = json["firstInt"] as? Int {
if let parsedSecondInt = json["secondInt"] as? Int {
self.combinedIntRequired = parsedFirstInt * parsedSecondInt
}
}
// Most succinct way to error if we weren't able to construct self.combinedIntRequired?
}
}
TypedObject(fromParsedJson: parsedJson)
There's a number of issues here that I'm hoping to work around:
It's extremely verbose, since I need to wrap every single property in a copy-pasted if-let for safety.
I'm not sure how to communicate errors when required properties are missing (as noted above). Swift seems to prefer (?) using exceptions for show-stopping problems (rather than pedestrian malformed data as here).
I don't know a nice way to deal with properties that exist but are the wrong type (given that the as? casting will fail and simply skip the block, it's not very informative to the user).
If I want to translate a few properties into a single one, I need to nest the let blocks proportional to the number of properties I'm combining. (This is probably more generally a problem with combining multiple optionals into one value safely).
In general, I'm writing imperative parsing logic when I feel like I ought to be able to do something a little more declarative (either with some stated JSON schema or at least inferring the schema from the class definition).

I do this using the Jastor framework:
1) Implement a Protocol that has a single function that returns an NSDictionary response:
protocol APIProtocol {
func didReceiveResponse(results: NSDictionary)
}
2) Create an API class that defines an NSURLConnection object that can be used as a Request URL for iOS's networking API. This class is created to simply return a payload from the itunes.apple.com API.
class API: NSObject {
var data: NSMutableData = NSMutableData()
var delegate: APIProtocol?
func searchItunesFor(searchTerm: String) {
// Clean up the search terms by replacing spaces with +
var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+",
options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music"
var url: NSURL = NSURL(string: urlPath)
var request: NSURLRequest = NSURLRequest(URL: url)
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
println("Search iTunes API at URL \(url)")
connection.start()
}
// NSURLConnection Connection failed.
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
println("Failed with error:\(error.localizedDescription)")
}
// New request so we need to clear the data object.
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.data = NSMutableData()
}
// Append incoming data.
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.data.appendData(data)
}
// NSURLConnection delegate function.
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Finished receiving data and convert it to a JSON object.
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
delegate?.didReceiveResponse(jsonResult)
}
}
3) Create a class with associated properties that inherits from Jastor
NSDictionary response:
{
"resultCount" : 50,
"results" : [
{
"collectionExplicitness" : "notExplicit",
"discCount" : 1,
"artworkUrl60" : "http:\/\/a4.mzstatic.com\/us\/r30\/Features\/2a\/b7\/da\/dj.kkirmfzh.60x60-50.jpg",
"collectionCensoredName" : "Changes in Latitudes, Changes in Attitudes (Ultmate Master Disk Gold CD Reissue)"
}
]
}
Music.swift
class Music : Jastor {
var resultCount: NSNumber = 0
}
4) Then in your ViewController be sure to set the delegate to self and then make a call to the API's searchITunesFor() method.
var api: API = API()
override func viewDidLoad() {
api.delegate = self;
api.searchItunesFor("Led Zeppelin")
}
5) Implement the Delegate method for didReceiveResponse(). Jastor extends your class to set a NSDictionary of the results returned from the iTunes API.
// #pragma - API Delegates
func didReceiveResponse(results: NSDictionary) {
let music = Music(dictionary: results)
println(music)
}

Short version: Since init isn't allowed to fail, validation has to happen outside of it. Optionals seem to be the intended tool for flow control in these cases. My solution is to use a factory method that returns an optional of the class, and use option chaining inside it to extract and validate the fields.
Note also that Int and Bool aren't children of AnyObject; data coming from an NSDictionary will have them stored as NSNumbers, which can't be cast directly to Swift types. Thus the calls to .integerValue and .boolValue.
Long version:
// Start with NSDictionary since that's what NSJSONSerialization will give us
var invalidJson: NSDictionary = [ "int" : 1, "nested" : [ "bool" : true ] ]
var validJson: NSDictionary = [
"int" : 1,
"nested" : [ "bool" : true ],
"firstInt" : 3,
"secondInt" : 5
]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(intValue: Int, combinedInt: Int, stringValue: String?, nestedBool: Bool?) {
self.intValueRequired = intValue
self.combinedIntRequired = combinedInt
// Use Optionals for the non-required parameters so
// we know whether to leave the default values in place
if let s = stringValue {
self.stringValueWithDefault = s
}
if let n = nestedBool {
self.nestedBoolBroughtToTopLevel = n
}
}
class func createFromDictionary(json: Dictionary<String, AnyObject>) -> TypedObject? {
// Validate required fields
var intValue: Int
if let x = (json["int"]? as? NSNumber)?.integerValue {
intValue = x
} else {
return nil
}
var combinedInt: Int
let firstInt = (json["firstInt"]? as? NSNumber)?.integerValue
let secondInt = (json["secondInt"]? as? NSNumber)?.integerValue
switch (firstInt, secondInt) {
case (.Some(let first), .Some(let second)):
combinedInt = first * second
default:
return nil
}
// Extract optional fields
// For some reason the compiler didn't like casting from AnyObject to String directly
let stringValue = json["string"]? as? NSString as? String
let nestedBool = (json["nested"]?["bool"]? as? NSNumber)?.boolValue
return TypedObject(intValue: intValue, combinedInt: combinedInt, stringValue: stringValue, nestedBool: nestedBool)
}
class func createFromDictionary(json: NSDictionary) -> TypedObject? {
// Manually doing this cast since it works, and the only thing Apple's docs
// currently say about bridging Cocoa and Dictionaries is "Information forthcoming"
return TypedObject.createFromDictionary(json as Dictionary<String, AnyObject>)
}
}
TypedObject.createFromDictionary(invalidJson) // nil
TypedObject.createFromDictionary(validJson) // it works!

I've also done the following to convert to/from:
class Image {
var _id = String()
var title = String()
var subTitle = String()
var imageId = String()
func toDictionary(dict dictionary: NSDictionary) {
self._id = dictionary["_id"] as String
self.title = dictionary["title"] as String
self.subTitle = dictionary["subTitle"] as String
self.imageId = dictionary["imageId"] as String
}
func safeSet(d: NSMutableDictionary, k: String, v: String) {
if (v != nil) {
d[k] = v
}
}
func toDictionary() -> NSDictionary {
let jsonable = NSMutableDictionary()
self.safeSet(jsonable, k: "title", v: self.title);
self.safeSet(jsonable, k: "subTitle", v: self.subTitle);
self.safeSet(jsonable, k: "imageId", v: self.imageId);
return jsonable
}
}
Then I simply do the following:
// data (from service)
let responseArray = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSArray
self.objects = NSMutableArray()
for item: AnyObject in responseArray {
var image = Image()
image.toDictionary(dict: item as NSDictionary)
self.objects.addObject(image)
}
If you want to POST the data:
var image = Image()
image.title = "title"
image.subTitle = "subTitle"
image.imageId = "imageId"
let data = NSJSONSerialization.dataWithJSONObject(image.toDictionary(), options: .PrettyPrinted, error: nil) as NSData
// data (to service)
request.HTTPBody = data;

Related

Swift 4 JSONSerialization.jsonObject

I am using Xcode 9.2 and Swift 4. How can I check if the returned json data is null?
Now it gives me error Type 'Any' has no subscript members at line if json[0]["data"]
var json: NSMutableArray = []
var newsArray: NSMutableArray = []
let url = URLFactory()
var data = try! NSData(contentsOf: url.getURL()) as Data
do {
json = try JSONSerialization.jsonObject(with: data, options: []) as! NSMutableArray
if json[0]["data"] {
// data is not null
}
} catch let error as NSError {
// handle error
}
My JSON returns something like this:
{
"data":
[
{
"news_id":123,
"title":"title",
"news_date":"2017-02-08 21:46:06",
"news_url":"url",
"short_description":"description",
"category_id":4,
"category_name":"Health",
"latlng":
[
{
"lat":"43.003429",
"lng":"-78.696335"
}
]
}
{ ....
}
If you're using Swift 4, I might suggest JSONDecoder:
First, define the types to hold the parsed data:
struct ResponseObject: Codable {
let data: [NewsItem]
}
struct NewsItem: Codable {
let newsId: Int
let title: String
let newsDate: Date
let newsURL: URL
let shortDescription: String
let categoryID: Int
let categoryName: String
let coordinates: [Coordinate]
// because your json keys don't follow normal Swift naming convention, use CodingKeys to map these property names to JSON keys
enum CodingKeys: String, CodingKey {
case newsId = "news_id"
case title
case newsDate = "news_date"
case newsURL = "news_url"
case shortDescription = "short_description"
case categoryID = "category_id"
case categoryName = "category_name"
case coordinates = "latlng"
}
}
struct Coordinate: Codable {
let lat: String
let lng: String
}
And then you can parse it:
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
do {
let responseObject = try decoder.decode(ResponseObject.self, from: data)
print(responseObject.data)
} catch {
print(error)
}
Clearly, if your JSON is different, you might need to change your objects accordingly (e.g. it struck me odd that latlng was an array of coordinates). Also, you can define custom init(from:) methods if you want to convert some of these strings into numbers, but I'd rather fix the JSON, instead (why are latlng returning string values rather than numeric values).
For more information, see Encoding and Decoding Custom Types.
As an aside, I'd advise against this pattern (note, this is your network request logic, but excised of NSData):
let data = try! Data(contentsOf: url.getURL())
That will retrieve the data synchronously, which can be problematic because
the app will be frozen while the data is being retrieved resulting in a poor UX;
you risk having your app killed by the watchdog process which looks for frozen apps; and
you don't have robust error handling and this will crash if the network request fails.
I'd suggest using URLSession:
let task = URLSession.shared.dataTask(with: url.getURL()) { data, _, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
// now parse `data` like shown above
// if you then need to update UI or model objects, dispatch that back
// to the main queue:
DispatchQueue.main.async {
// use `responseObject.data` to update model objects and/or UI here
}
}
task.resume()

Vapor JSON from `[String: Any]` Dictionary

If I build a Swift dictionary, i.e. [String: Any] how can I return that as JSON? I tried this, but it gives me the error: Argument labels '(node:)' do not match any available overloads.
drop.get("test") { request in
var data: [String: Any] = [:]
data["name"] = "David"
data["state"] = "CA"
return try JSON(node: data)
}
Convoluted as heck, but this allows you to use [String:Any].makeNode(), as long as the internals are NodeRepresentable, NSNumber based, or NSNull :) --
import Node
enum NodeConversionError : LocalizedError {
case invalidValue(String,Any)
var errorDescription: String? {
switch self {
case .invalidValue(let key, let value): return "Value for \(key) is not NodeRepresentable - " + String(describing: type(of: value))
}
}
}
extension NSNumber : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.number(.double(Double(self)))
}
}
extension NSString : NodeRepresentable {
public func makeNode(context: Context = EmptyNode) throws -> Node {
return Node.string(String(self))
}
}
extension KeyAccessible where Key == String, Value == Any {
public func makeNode(context: Context = EmptyNode) throws -> Node {
var mutable: [String : Node] = [:]
try allItems.forEach { key, value in
if let _ = value as? NSNull {
mutable[key] = Node.null
} else {
guard let nodeable = value as? NodeRepresentable else { throw NodeConversionError.invalidValue(key, value) }
mutable[key] = try nodeable.makeNode()
}
}
return .object(mutable)
}
public func converted<T: NodeInitializable>(to type: T.Type = T.self) throws -> T {
return try makeNode().converted()
}
}
With that header you can:
return try JSON(node: data.makeNode())
JSON cannot be initialized from a [String : Any] dictionary because Any is not convertible to Node.
There are only a limited number of types that Node can be. (See Node source). If you know your objects are all going to be the same type, use a dictionary that only allows that type. So for your example, [String : String].
If you're going to be getting data from the request, you can try using request.json as is used in the documentation here.
EDIT:
Another (possibly better) solution would be to make your dictionary [String: Node] and then you can include any type that conforms to Node. You may have to call the object's makeNode() function to add it to the dictionary though.

How to convert a Swift object to a dictionary

I'm relatively new to iOS programming. However, I would have assumed that Swift would have an automated way of converting objects to JSON and vice versa. That being said, I have found several libraries that can do this.
HOWEVER...
It seems that no matter how you post data to a web service (even using something like AlamoFire), the requests must be a dictionary. All these forums show examples of how easy it is to convert the returned JSON string to objects. True. But the request needs to be manually coded. That is, go through all of the object properties and map them as a dictionary.
So my question is this: Am I missing something? Have I got this all wrong and there's a super-easy way to either (a) send JSON (instead of a dictionary) in the REQUEST or (b) convert an object automatically to a dictionary?
Again, I see how easy it is to deal with a JSON response. I'm just looking for an automatic way to convert the request object I want to post to a web service into a format that a library like AlamoFire (or whatever) requires. With other languages this is fairly trivial, so I'm hoping there's an equally easy and automated way with Swift.
I must disagree with #Darko.
In Swift 2,
use protocol oriented programming and the simple reflection offered by Mirror class :
protocol JSONAble {}
extension JSONAble {
func toDict() -> [String:Any] {
var dict = [String:Any]()
let otherSelf = Mirror(reflecting: self)
for child in otherSelf.children {
if let key = child.label {
dict[key] = child.value
}
}
return dict
}
}
then you can use this protocol with your request class and produce the desired dictionary :
class JsonRequest : JSONAble {
var param1 : String?
// ...
}
let request = JsonRequest()
// set params of the request
let dict = request.toDict()
// use your dict
My solution to this will be something like this:
extension Encodable {
var dict : [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else { return nil }
return json
}
}
and usage will be something like this:
movies.compactMap { $0.dict }
Swift currently does not support advanced reflection like Java or C# so the answer is: no, there is not an equally easy and automated way with pure Swift.
[Update] Swift 4 has meanwhile the Codable protocol which allows serializing to/from JSON and PLIST.
typealias Codable = Decodable & Encodable
Without using reflection, and works for nested objects (Swift 4):
protocol Serializable {
var properties:Array<String> { get }
func valueForKey(key: String) -> Any?
func toDictionary() -> [String:Any]
}
extension Serializable {
func toDictionary() -> [String:Any] {
var dict:[String:Any] = [:]
for prop in self.properties {
if let val = self.valueForKey(key: prop) as? String {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Int {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Double {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Array<String> {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Serializable {
dict[prop] = val.toDictionary()
} else if let val = self.valueForKey(key: prop) as? Array<Serializable> {
var arr = Array<[String:Any]>()
for item in (val as Array<Serializable>) {
arr.append(item.toDictionary())
}
dict[prop] = arr
}
}
return dict
}
}
Just implement properties and valueForKey for the custom objects you want to convert. For example:
class Question {
let title:String
let answer:Int
init(title:String, answer:Int) {
self.title = title
self.answer = answer
}
}
extension Question : Serializable {
var properties: Array<String> {
return ["title", "answer"]
}
func valueForKey(key: String) -> Any? {
switch key {
case "title":
return title
case "answer":
return answer
default:
return nil
}
}
}
You can add more value types in the toDictionary function if you need.
The latest solution that I found after lots of digging throughout Stack Overflow is:
//This block of code used to convert object models to json string
let jsonData = try JSONEncoder().encode(requestData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
//This method is used to convert jsonstring to dictionary [String:Any]
func jsonToDictionary(from text: String) -> [String: Any]? {
guard let data = text.data(using: .utf8) else { return nil }
let anyResult = try? JSONSerialization.jsonObject(with: data, options: [])
return anyResult as? [String: Any]
}
//Use above method something like this
let params = jsonToDictionary(from: jsonString) ?? [String : Any]()
//Use params to pass in paramters
Alamofire.request(completeUrl, method: .post, parameters: params, encoding:JSONEncoding.prettyPrinted, headers: myHeaders){
response in
//Do whatever you want with response of it.
}
Note:
I combine this solution from multiple answers.
This solution i used with alamofire because alamofire only accept parameter at this format "[String:Any]".
You can also use the ObjectMapper library. It has a "toJSON" method that converts your object to a dictionary.
in short
let dict = Mirror(reflecting: self).children.map({ $0 }).reduce(into: [:]) { $0[$1.label] = $1.value }
Example how to use Mirror with conversion to specific Dictionary type:
protocol DictionaryConvertible { }
extension DictionaryConvertible {
func toDictionary() -> [String: CustomStringConvertible] {
Dictionary(
uniqueKeysWithValues: Mirror(reflecting: self).children
.compactMap { child in
if let label = child.label,
let value = child.value as? CustomStringConvertible {
return (label, value)
} else {
return nil
}
}
)
}
}

Swift creating JSON data from AnyObject

Unfortunately I didn't find anything useful for writing JSON data on the internet. I wanted to ask what I have to do to let a class become a 'valid JSON object' that can be converted to JSON using NSJSONSerialization.dataWithJSONObject(<AnyObject>, options: .allZeros, error: nil). Does data have to be either a value like String or NSNumber or an object such as Array<AnyObject> or Dictionary<String, AnyObject>, which are objects that can easily be represented in JSON or are there any other possibilities to convert a JSON object? Thanks for your answers
NSJSONSerialization Class Reference:
An object that may be converted to JSON must have the following
properties:
The top level object is an NSArray or NSDictionary.
All objects are instances of NSString, NSNumber, NSArray,
NSDictionary, or NSNull.
All dictionary keys are instances of NSString.
Numbers are not NaN or infinity.
If you have other objects you have to manually convert them to one of these basic objects. So basically String and NSNumber. As far as I know there is no protocol that you can implement to turn your class into a JSONSerializable object.
In my opinion the best way to get your own objects into JSON is to create a method that returns a dictionary that only contains the allowed types. For the opposite way you can create a init that takes a dictionary.
Such an implementation would look like this:
class FancyObject {
let color: UIColor
let creationDate: NSDate
let title: String
init(title: String, color: UIColor) {
self.color = color
self.title = title
self.creationDate = NSDate()
}
init(dictionary: [String : AnyObject]) {
let colorString = dictionary["color"] as String
self.color = UIColor(rgbaString: colorString)!
self.title = dictionary["title"] as String
let dateString = dictionary["creationDate"] as String
self.creationDate = posixDateFormatter().dateFromString(dateString)!
}
func jsonRepresentation() -> [String : AnyObject] {
var json = [String : AnyObject]()
json["color"] = self.color.rgbaString()
json["title"] = self.title
json["creationDate"] = posixDateFormatter().stringFromDate(self.creationDate)
return json
}
}
If you want to play around in the Playground here is the other stuff that is referenced:
extension UIColor {
func rgbaString() -> String? {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
if self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
return "\(red),\(green),\(blue),\(alpha)"
}
return nil
}
public convenience init?(rgbaString: String) {
var red: Float = 0
var green: Float = 0
var blue: Float = 0
var alpha: Float = 1
let scanner = NSScanner(string: rgbaString)
scanner.scanFloat(&red)
scanner.scanString(",", intoString: nil)
scanner.scanFloat(&green)
scanner.scanString(",", intoString: nil)
scanner.scanFloat(&blue)
scanner.scanString(",", intoString: nil)
scanner.scanFloat(&alpha)
self.init(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha))
}
}
func posixDateFormatter() -> NSDateFormatter {
let posixDateFormatter = NSDateFormatter()
posixDateFormatter.locale = NSLocale(localeIdentifier:"en_US_POSIX")
posixDateFormatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
posixDateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
return posixDateFormatter
}
let originalObject = FancyObject(title: "Object 1", color: UIColor.magentaColor())
let originalJSONDictionary = originalObject.jsonRepresentation()
var error: NSError?
let json = NSJSONSerialization.dataWithJSONObject(originalJSONDictionary, options: NSJSONWritingOptions.PrettyPrinted, error: &error)
if json == nil {
println("Can't create json \(error)")
}
/* JSON:
{
"title" : "Object 1",
"creationDate" : "2015-03-14T23:04:35Z",
"color" : "1.0,0.0,1.0,1.0"
}
*/
let dictionaryFromJSON = NSJSONSerialization.JSONObjectWithData(json!, options: nil, error: &error) as? [String : AnyObject]
if dictionaryFromJSON == nil {
println("Can't create dict \(error)")
}
let objectFromJSON = FancyObject(dictionary: dictionaryFromJSON!)
According to the docs the definitive way is to call isValidJSONObject: or try:
An object that may be converted to JSON must have the following
properties:
The top level object is an NSArray or NSDictionary.
All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
All dictionary keys are instances of NSString.
Numbers are not NaN or infinity.
Other rules may apply. Calling isValidJSONObject: or attempting a
conversion are the definitive ways to tell if a given object can be
converted to JSON data.

Deserialize JSON / NSDictionary to Swift objects

Is there a way to properly deserialize a JSON response to Swift objects resp. using DTOs as containers for fixed JSON APIs?
Something similar to http://james.newtonking.com/json or something like this example from Java
User user = jsonResponse.readEntity(User.class);
whereby jsonResponse.toString() is something like
{
"name": "myUser",
"email": "user#example.com",
"password": "passwordHash"
}
SWIFT 4 Update
Since you give a very simple JSON object the code prepared for to handle that model. If you need more complicated JSON models you need to improve this sample.
Your Custom Object
class Person : NSObject {
var name : String = ""
var email : String = ""
var password : String = ""
init(JSONString: String) {
super.init()
var error : NSError?
let JSONData = JSONString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
let JSONDictionary: Dictionary = NSJSONSerialization.JSONObjectWithData(JSONData, options: nil, error: &error) as NSDictionary
// Loop
for (key, value) in JSONDictionary {
let keyName = key as String
let keyValue: String = value as String
// If property exists
if (self.respondsToSelector(NSSelectorFromString(keyName))) {
self.setValue(keyValue, forKey: keyName)
}
}
// Or you can do it with using
// self.setValuesForKeysWithDictionary(JSONDictionary)
// instead of loop method above
}
}
And this is how you invoke your custom class with JSON string.
override func viewDidLoad() {
super.viewDidLoad()
let jsonString = "{ \"name\":\"myUser\", \"email\":\"user#example.com\", \"password\":\"passwordHash\" }"
var aPerson : Person = Person(JSONString: jsonString)
println(aPerson.name) // Output is "myUser"
}
I recommend that you use code generation (http://www.json4swift.com) to create native models out of the json response, this will save your time of parsing by hand and reduce the risk of errors due to mistaken keys, all elements will be accessible by model properties, this will be purely native and the models will make more sense rather checking the keys.
Your conversion will be as simple as:
let userObject = UserClass(userDictionary)
print(userObject!.name)
Swift 2: I really like the previous post of Mohacs! To make it more object oriented, i wrote a matching Extension:
extension NSObject{
convenience init(jsonStr:String) {
self.init()
if let jsonData = jsonStr.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
{
do {
let json = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as! [String: AnyObject]
// Loop
for (key, value) in json {
let keyName = key as String
let keyValue: String = value as! String
// If property exists
if (self.respondsToSelector(NSSelectorFromString(keyName))) {
self.setValue(keyValue, forKey: keyName)
}
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
}
else
{
print("json is of wrong format!")
}
}
}
custom classes:
class Person : NSObject {
var name : String?
var email : String?
var password : String?
}
class Address : NSObject {
var city : String?
var zip : String?
}
invoking custom classes with JSON string:
var jsonString = "{ \"name\":\"myUser\", \"email\":\"user#example.com\", \"password\":\"passwordHash\" }"
let aPerson = Person(jsonStr: jsonString)
print(aPerson.name!) // Output is "myUser"
jsonString = "{ \"city\":\"Berlin\", \"zip\":\"12345\" }"
let aAddress = Address(jsonStr: jsonString)
print(aAddress.city!) // Output is "Berlin"
Yet another JSON handler I wrote:
https://github.com/dankogai/swift-json
With it you can go like this:
let obj:[String:AnyObject] = [
"array": [JSON.null, false, 0, "", [], [:]],
"object":[
"null": JSON.null,
"bool": true,
"int": 42,
"double": 3.141592653589793,
"string": "a α\t弾\n𪚲",
"array": [],
"object": [:]
],
"url":"http://blog.livedoor.com/dankogai/"
]
let json = JSON(obj)
json.toString()
json["object"]["null"].asNull // NSNull()
json["object"]["bool"].asBool // true
json["object"]["int"].asInt // 42
json["object"]["double"].asDouble // 3.141592653589793
json["object"]["string"].asString // "a α\t弾\n𪚲"
json["array"][0].asNull // NSNull()
json["array"][1].asBool // false
json["array"][2].asInt // 0
json["array"][3].asString // ""
As you see no !? needed between subscripts.
In addition to that you can apply your own schema like this:
//// schema by subclassing
class MyJSON : JSON {
override init(_ obj:AnyObject){ super.init(obj) }
override init(_ json:JSON) { super.init(json) }
var null :NSNull? { return self["null"].asNull }
var bool :Bool? { return self["bool"].asBool }
var int :Int? { return self["int"].asInt }
var double:Double? { return self["double"].asDouble }
var string:String? { return self["string"].asString }
var url: String? { return self["url"].asString }
var array :MyJSON { return MyJSON(self["array"]) }
var object:MyJSON { return MyJSON(self["object"]) }
}
let myjson = MyJSON(obj)
myjson.object.null // NSNull?
myjson.object.bool // Bool?
myjson.object.int // Int?
myjson.object.double // Double?
myjson.object.string // String?
myjson.url // String?
There's a great example by Apple for deserializing JSON with Swift 2.0
The trick is to use the guard keyword and chain the assignments like so:
init?(attributes: [String : AnyObject]) {
guard let name = attributes["name"] as? String,
let coordinates = attributes["coordinates"] as? [String: Double],
let latitude = coordinates["lat"],
let longitude = coordinates["lng"],
else {
return nil
}
self.name = name
self.coordinates = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
I personally prefer native parsing vs any 3rd party, as it is transparent and magic-less. (and bug less?)
Using quicktype, I generated your model and serialization helpers from your sample:
import Foundation
struct User: Codable {
let name: String
let email: String
let password: String
}
extension User {
static func from(json: String, using encoding: String.Encoding = .utf8) -> OtherUser? {
guard let data = json.data(using: encoding) else { return nil }
return OtherUser.from(data: data)
}
static func from(data: Data) -> OtherUser? {
let decoder = JSONDecoder()
return try? decoder.decode(OtherUser.self, from: data)
}
var jsonData: Data? {
let encoder = JSONEncoder()
return try? encoder.encode(self)
}
var jsonString: String? {
guard let data = self.jsonData else { return nil }
return String(data: data, encoding: .utf8)
}
}
Then parse User values like this:
let user = User.from(json: """{
"name": "myUser",
"email": "user#example.com",
"password": "passwordHash"
}""")!
I wrote this small open-source library recently that lets you quickly and easily deserialize dictionaries into Swift objects: https://github.com/isair/JSONHelper
Using it, deserializing data becomes as easy as this:
var myInstance = MyClass(data: jsonDictionary)
or
myInstance <-- jsonDictionary
And models need to look only like this:
struct SomeObjectType: Deserializable {
var someProperty: Int?
var someOtherProperty: AnotherObjectType?
var yetAnotherProperty: [YetAnotherObjectType]?
init(data: [String: AnyObject]) {
someProperty <-- data["some_key"]
someOtherProperty <-- data["some_other_key"]
yetAnotherProperty <-- data["yet_another_key"]
}
}
Which, in your case, would be:
struct Person: Deserializable {
var name: String?
var email: String?
var password: String?
init(data: [String: AnyObject]) {
name <-- data["name"]
email <-- data["email"]
password <-- data["password"]
}
}
If you would like parse from and to json without the need to manually map keys and fields, then you could also use EVReflection. You can then use code like:
var user:User = User(json:jsonString)
or
var jsonString:String = user.toJsonString()
The only thing you need to do is to use EVObject as your data objects base class.
See the GitHub page for more detailed sample code
I am expanding upon Mohacs and Peter Kreinz's excellent answers just a bit to cover the array of like objects case where each object contains a mixture of valid JSON data types. If the JSON data one is parsing is an array of like objects containing a mixture of JSON data types, the do loop for parsing the JSON data becomes this.
// Array of parsed objects
var parsedObjects = [ParsedObject]()
do {
let json = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as [Dictionary<String, AnyObject>]
// Loop through objects
for dict in json {
// ParsedObject is a single instance of an object inside the JSON data
// Its properties are a mixture of String, Int, Double and Bool
let parsedObject = ParsedObject()
// Loop through key/values in object parsed from JSON
for (key, value) in json {
// If property exists, set the value
if (parsedObject.respondsToSelector(NSSelectorFromString(keyName))) {
// setValue can handle AnyObject when assigning property value
parsedObject.setValue(keyValue, forKey: keyName)
}
}
parsedObjects.append(parsedObject)
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
This way lets you get the user from a URL. It's parse the NSData to a NSDictionary and then to your NSObject.
let urlS = "http://api.localhost:3000/"
func getUser(username: Strung) -> User {
var user = User()
let url = NSURL(string: "\(urlS)\(username)")
if let data = NSData(contentsOfURL: url!) {
setKeysAndValues(user, dictionary: parseData(data))
}
return user
}
func setKeysAndValues (object : AnyObject, dictionary : NSDictionary) -> AnyObject {
for (key, value) in dictionary {
if let key = key as? String, let value = value as? String {
if (object.respondsToSelector(NSSelectorFromString(key))) {
object.setValue(value, forKey: key)
}
}
}
return object
}
func parseData (data : NSData) -> NSDictionary {
var error: NSError?
return NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as! NSDictionary
}
In Swift 4, You can use the Decoding, CodingKey protocols to deserialize the JSON response:
Create the class which confirm the decodable protocol
class UserInfo: Decodable
Create members of the class
var name: String
var email: String
var password: String
Create JSON key enum which inherits from CodingKey
enum UserInfoCodingKey: String, CodingKey {
case name
case password
case emailId
}
Implement init
required init(from decoder: Decoder) throws
The whole class look like :
Call Decoder
// jsonData is JSON response and we get the userInfo object
let userInfo = try JsonDecoder().decode(UserInfo.self, from: jsonData)
You do this by using NSJSONSerialization. Where data is your JSON.
First wrap it in an if statement to provide some error handling capablity
if let data = data,
json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] {
// Do stuff
} else {
// Do stuff
print("No Data :/")
}
then assign them:
let email = json["email"] as? String
let name = json["name"] as? String
let password = json["password"] as? String
Now, This will show you the result:
print("Found User iname: \(name) with email: \(email) and pass \(password)")
Taken from this Swift Parse JSON tutorial. You should check out the tutorial as it goes a lot more in depth and covers better error handling.