58

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"
}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
dimitri
  • 623
  • 1
  • 6
  • 5
  • 1
    `NSJSONSerialization`, here the docs: https://developer.apple.com/library/prerelease/ios/documentation/Foundation/Reference/NSJSONSerialization_Class/index.html – holex Jun 19 '14 at 15:29
  • This is to convert NSDictionary to JSON or vice versa, I'm looking for *custom* swift objects – dimitri Jun 19 '14 at 15:31
  • 1
    what _custom_ swift objects? – holex Jun 19 '14 at 15:33
  • 1
    why can't you just convert to a dictionary and then pull the values out of the dictionary and build your objects? – Lance Jun 19 '14 at 15:39
  • 1
    holex: for example, I have a User.swift with properties, User.swift would be my object – dimitri Jun 19 '14 at 15:45
  • 3
    Lance of course I can, but if there's an elegant solution I would avoid having to map every field redundantly - that way if my json response has more fields, I don't need to maintain my custom object.. – dimitri Jun 19 '14 at 15:45
  • I have seen Objective-C libraries that translate "intelligently" between JSON and custom object classes, but I do not remember the name. - Pretty sure none of them has been translated to Swift yet. – Martin R Jun 19 '14 at 15:59
  • Tougher to do in swift, because as I recall from several questions, there's no way to create an object by dynamic name, although I suppose that you could still fall-back to objective C for that part. – David Berry Jun 19 '14 at 17:30
  • Martin R that's what I thought - any suggestions on the obj-c libraries that perform well? – dimitri Jun 19 '14 at 17:31
  • +1 as the question is completely legitime and for sb. with a Java background I'm asking myself why I only saw examples like json["myMagicFieldThatMightNotEvenExistInTheReturnedJSON"] so far – Alex Jul 16 '14 at 11:21
  • 1
    You don't need a library, just unserialise the data into a dictionary, create an object, and set all the values. That's a few lines of code. – Abhi Beckert Aug 10 '14 at 11:58
  • @AbhiBeckert It is handling all the Optionals when digging data out several levels deep. See the link in the answer by xmkevinchen point to SwiftyJSON. This is not coder-friendly™: `if let userName = (((jsonObject as? NSArray)?[0] as? NSDictionary)?["user"] as? NSDictionary)?["name"]{`. Only the inventor of Swift could like that. What we want is something like this: `if let userName = json[0]["user"]["name"].string{` – zaph Aug 11 '14 at 22:02
  • @dimitri good question, but don't you need to a serialize solution also if you're updating the objects? – whitneyland Oct 13 '14 at 17:40
  • Check out https://github.com/Mantle/Mantle – DarkLeafyGreen Nov 03 '14 at 18:50

12 Answers12

61

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"
}
modusCell
  • 13,151
  • 9
  • 53
  • 80
  • 1
    This requires me to use KVO and thus inherit from NSObject, it would be beneficial to not require inheriting from NSObject at least. – Benjamin Gruenbaum Aug 14 '14 at 13:55
  • 4
    There is no way in a "pure" Swift manner to map from the string "email" to the email property as there is no introspection. It will not be possible to do what you are asking for without using KVO and as you have surmised KVO currently requires you to rely on Objective-C. – Scott Thompson Aug 15 '14 at 02:24
  • This code requires updating to be compatible with the latest version of Swift – William Entriken Dec 20 '15 at 06:04
  • 1
    ` let JSONDictionary: Dictionary = NSJSONSerialization.JSONObjectWithData(JSONData, options: nil, error: &error) as NSDictionary` No longer takes an *error* argument – DanHabib Apr 28 '16 at 16:20
  • How about nested json? – Sazzad Hissain Khan Mar 02 '20 at 11:06
10

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)
Syed Absar
  • 2,274
  • 1
  • 26
  • 45
9

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"
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
7

Yet another JSON handler I wrote:

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?
dankogai
  • 1,627
  • 12
  • 7
5

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?)

AmitP
  • 5,353
  • 4
  • 35
  • 27
4

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"
}""")!
David Siegel
  • 1,604
  • 11
  • 13
  • what happens if the api now has a new property field for User called "JobTitle", I have to manually add to User class the property, let Jobtitle: String <--- I wanted to avoid having to have properties, and access the api's response data dynamically, strongly typed – dimitri Oct 22 '17 at 00:21
  • What does your program do? If the API adds a field you will not use, this code will still work; if the API adds a field you intend to use, are you saying you'd prefer to access it dynamically rather than adding a field to `User`? Unfortunately, there is no meaning of 'dynamically, strongly typed' as far as I know. – David Siegel Oct 22 '17 at 01:03
3

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"]
    }
}
isair
  • 1,790
  • 15
  • 15
3

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

Edwin Vermeer
  • 13,017
  • 2
  • 34
  • 58
2

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)")
}
jkwuc89
  • 1,345
  • 14
  • 20
1

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
}
Eduardo Irias
  • 1,043
  • 11
  • 12
1

In Swift 4, You can use the Decoding, CodingKey protocols to deserialize the JSON response:

  1. Create the class which confirm the decodable protocol

    class UserInfo: Decodable

  2. Create members of the class

    var name: String

    var email: String

    var password: String

  3. Create JSON key enum which inherits from CodingKey

    enum UserInfoCodingKey: String, CodingKey { case name case password case emailId }

  4. Implement init

    required init(from decoder: Decoder) throws

    The whole class look like :

    enter image description here

  5. Call Decoder

    // jsonData is JSON response and we get the userInfo object

    let userInfo = try JsonDecoder().decode(UserInfo.self, from: jsonData)

yo2bh
  • 1,356
  • 1
  • 14
  • 26
0

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.

MarkP
  • 2,546
  • 5
  • 31
  • 48