Can I set values for a Swift object properties using mirroring? - json

Right now I can inspect variables of an object using Mirror type. But can I set values for my variables using mirroring? Or maybe there's another pure-Swift way?
For example, I'd like to create an object (a Swift struct) from JSON. Is it possible without subclassing NSObject and using Objective-C functions for that?

This was the best I can do at the moment. It is still missing converting the mirrorObject back to its generic type. FYI this is using SwiftyJSON
func convertToObject<T>(json: JSON, genericObject: T) -> T {
let mirroredObject = Mirror(reflecting: genericObject)
for (_, var attr) in mirroredObject.children.enumerate() {
if let propertyName = attr.label as String! {
attr.value = json[propertyName]
print(propertyName)
print(attr.value)
}
}
// Figure out how to convert back to object type...
}

This is an old question, but the answer was not working for me.
I had to change my swift object to a NSObject to make things work, and also to have dynamic properties.
In my case I use the pod Marshal to deserialize Data.
class MyClass: NSObject, Unmarshaling
{
// #objc dynamic make property available for NSObject
#objc dynamic var myProperty: String?
required init(object: MarshaledObject) throws {
super.init()
initUsingReflection(object: object)
}
func initUsingReflection(object: MarshaledObject) {
let mirror = Mirror(reflecting: self)
// we go through children
for child in mirror.children {
guard let key = child.label else {
continue
}
// This line is here to get the value from json, in my case I already know the type I needed
let myValue: String = try! object.value(for: key)
// The trick is here, setValue only exist in NSObject and not in swift object.
self.setValue(myValue, forKey: key)
}
}
}

Related

JSON to dict with class

I decide some JSON and try to typecast it to a dictionary of String: classy and it fails. I have found that often the reason I have trouble doing something is because of a misunderstanding of how Swift works, so here is what I want to happen. Feel free to tell me that I am doing it wrong and if I do it this way all will be wonderful.
I want my data to live between runs of the app so I have to save the data to storage between runs. I have an object, data and associated code, and I have places where changes I make to a copy should reflect back to the original so it is a class. I have a bunch of these objects and most of the time I pick the one I want based on an id that is an integer. An array is not good since it would be a sparse array cause come ids are not used. I came up with a dictionary with a key of the id and data of the structure. I turned the key from an Int to a String, by changing the Int id to a String, cause converting a dictionary to JSON is MUCH easier for a key that is a string. I save the JSON string. When the app starts again I read the string in and convert the JSON string to Any. Then I typecast the result to the desired dictionary. This is where it fails. The cast does not work. In my Googling the samples I found said this should work.
Here is my code:
class Y: Codable, Hashable {
var a: String = "c"
static func ==(lhs: Y, rhs: Y) -> Bool {
return lhs.a == rhs.a
}
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
struct ContentView: View {
var body: some View {
VStack {
Button ("Error") {
var y = Y()
var yDict = [String: Y]()
yDict["0"] = y
do {
let encodedData = try JSONEncoder().encode(yDict)
let jsonString = String(data: encodedData, encoding: .utf8)
let decoded = try JSONSerialization.jsonObject(with: encodedData, options: [])
if let yyDictDec = decoded as? [String:Y] {
print("yDict after decide")
print (yyDictDec)
}
} catch {
print(error.localizedDescription)
}
print("x")
}
}
}
}
In this code the if yyDictDec = is failing, I think, cause the prints after it never happen. I can cast it as [String, Any] but I really need it to be my class.
My problem is in the convert JSON back to the dictionary. I feel I am missing something fairly simple.
Don´t use JSONSerialization use JsonDecoder and decode it to the the type it was before encoding. e.g.:
let decoded = try JSONDecoder().decode([String: Y].self, from: encodedData)

Parse JSON with variable keys

I'm trying to parse JSON with currency rates contains dynamic keys and a dynamic number of values. Output depends on input parameters, such as Base currency and several currencies to compare.
Example of JSON:
{
"USD_AFN": 70.129997,
"USD_AUD": 1.284793,
"USD_BDT": 82.889999,
"USD_BRL": 3.418294,
"USD_KHR": 4004.99952
}
, or:
{
"EUR_CAD": 0.799997
}
Also, I should be able to change Base currency and currencies to compare, and change number of currencies to compare.
I already tried this answer.
What is the optimal way to handle it?
Thanks
Additional info
So, I made the struct without the initializer
struct CurrencyRate: Codable {
var results : [String:Double]
}
and trying to decode it
do { let results = try decoder.decode(CurrencyRate.self, from: dataToDecode) print(results) } catch { print("Error") }
I'm still getting the error.
Eventually, I just need an array of currency rates (values) to populate it in a Table View.
After some experimentation my Playground looks as follows:
import Cocoa
import Foundation
let jsonData = """
{
"USD_AFN": 70.129997,
"USD_AUD": 1.284793,
"USD_BDT": 82.889999,
"USD_BRL": 3.418294,
"USD_KHR": 4004.99952
}
""".data(using: .utf8)!
do {
let obj = try JSONSerialization.jsonObject(with:jsonData, options:[])
print(obj) // this is an NSDictionary
if let dict = obj as? [String:Double] {
print(dict) // This is not "just" a cast ... more than I thought
}
}
struct CurrencyRate: Codable {
var results : [String:Double]
}
// If you use a "results"-key it _must_ be present in your JSON, but it would allow to add methods
let resultsJson = """
{
"results" : {
"USD_AFN": 70.129997,
"USD_AUD": 1.284793,
"USD_BDT": 82.889999,
"USD_BRL": 3.418294,
"USD_KHR": 4004.99952
}
}
""".data(using: .utf8)!
do {
let currencyRate = try JSONDecoder().decode(CurrencyRate.self, from: resultsJson)
print(currencyRate)
}
// this is probably the easiest solution for just reading it
do {
let rates = try JSONDecoder().decode([String:Double].self, from:jsonData)
print(rates)
}
// While you could do the following it does not feel "proper"
typealias CurrencyRatesDict = [String:Double]
extension Dictionary where Key == String, Value == Double {
func conversionRate(from:String, to:String) -> Double {
let key = "\(from)_\(to)"
if let rate = self[key] {
return rate
} else {
return -1.0
}
}
}
do {
let currRates = try JSONDecoder().decode(CurrencyRatesDict.self, from:jsonData)
print(currRates)
print(currRates.conversionRate(from:"USD", to:"AUD"))
}
This taught me a few things. I would not have thought that a NSDictionary (which is produced by JSONSerialization.jsonObject automatically and has no types) converts this easily into a [String:Double], but of course it might fail and you should write some error handling to catch it.
Your CurrencyRate struct would have the advantage to allow easy extensions. Since Dictionaries are structs it is not possible to derive from them. As the last version illustrates it is possible to add a conditional extension to a Dictionary. However this would add your new function to any Dictionary matching the signature which might be acceptable in many cases even though it 'feels' wrong from the design perspective.
As you can see there is a whole bunch of ways to deal with this in Swift. I would suggest you use the Codable protocol and an additional key. Most probably there are "other things" you will want to do with your object.

Use generic argument in method body <T>

I'm trying to create a generic method like the following:
private func map<T>(type:T, jsonString:String) -> T
{
do
{
let model = try Mapper<type>().map(JSONString: jsonString)!
return model
}
catch
{
Log.error("Failed to convert JSON jsonString to model object: \(jsonString)")
}
return EmptyModel()
}
but it result in compile error:
Error: use of undeclared type 'type'
How can I change it to use the specified type (a class object) with the Mapper's generic value?
You can use T instead of type:
let model = try Mapper<T>().map(JSONString: jsonString)!
You might want to change method signature, so it returns an instance of T and not the type T itself:
private func map<T>(type: T.Type, jsonString: String) -> T
That being said, Swift already has its JSONDecoder. It might already support what you are trying to implement.
let decoder = JSONDecoder()
let model = try decoder.decode(Model.self, from: data)

How to map dynamic properties in model class (Swift)

I am new to IOS Development, i came across a really interesting situation, i have this json response coming server side
{
"caps": {
"first_key": "34w34",
"first_char": "34w45",
"first_oddo": "34w34"
.... : .....
.... : .....
}
}
My issue this that keys inside "caps" object can be dynamic (like what if one more value is added). I am using ObjectMapper to mapper to map values from response to model class. I have this model class
class User: Mappable {
var first_key: String?
var first_char: String?
var first_oddo: String?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
first_key <- map["first_key"]
first_char <- map["first_char"]
first_oddo <- map["first_oddo"]
}
}
Now as i dont know how to populate my model if values in json response are changed (because that are dynamic). I hope i have explained it well. I think i dont want hard coded values inside model?
This solution uses the Codable protocol introduced with Swift 4.
If the keys of your JSON are dynamic then you need a Dictionary.
JSON
Given this JSON
let data = """
{
"caps": {
"first_key": "34w34",
"first_char": "34w45",
"first_oddo": "34w34"
}
}
""".data(using: .utf8)!
The Response Model
You can define a struct like this
struct Response:Codable {
let caps: [String:String]
}
Decoding 🎉🎉🎉
Now you can decode your JSON
if let response = try? JSONDecoder().decode(Response.self, from: data) {
print(response.caps)
}
Output
["first_key": "34w34", "first_oddo": "34w34", "first_char": "34w45"]

Automatic JSON serialization and deserialization of objects in Swift

I'm looking for a way to automatically serialize and deserialize class instances in Swift. Let's assume we have defined the following class …
class Person {
let firstName: String
let lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
… and Person instance:
let person = Person(firstName: "John", lastName: "Doe")
The JSON representation of person would be the following:
{
"firstName": "John",
"lastName": "Doe"
}
Now, here are my questions:
How can I serialize the person instance and get the above JSON without having to manually add all properties of the class to a dictionary which gets turned into JSON?
How can I deserialize the above JSON and get back an instantiated object that is statically typed to be of type Person? Again, I don't want to map the properties manually.
Here's how you'd do that in C# using Json.NET:
var person = new Person("John", "Doe");
string json = JsonConvert.SerializeObject(person);
// {"firstName":"John","lastName":"Doe"}
Person deserializedPerson = JsonConvert.DeserializeObject<Person>(json);
As shown in WWDC2017 # 24:48 (Swift 4), we will be able to use the Codable protocol. Example
public struct Person : Codable {
public let firstName:String
public let lastName:String
public let location:Location
}
To serialize
let payload: Data = try JSONEncoder().encode(person)
To deserialize
let anotherPerson = try JSONDecoder().decode(Person.self, from: payload)
Note that all properties must conform to the Codable protocol.
An alternative can be JSONCodable which is used by Swagger's code generator.
You could use EVReflection for that. You can use code like:
var person:Person = Person(json:jsonString)
or
var jsonString:String = person.toJsonString()
See the GitHub page for more detailed sample code. You only have to make EVObject the base class of your data objects. No mapping is needed (as long as the json keys are the same as the property names)
Update: Swift 4 has support for Codable which makes it almost as easy as EVReflection but with better performance. If you do want to use an easy contractor like above, then you could use this extension: Stuff/Codable
With Swift 4, you simply have to make your class conform to Codable (Encodable and Decodable protocols) in order to be able to perform JSON serialization and deserialization.
import Foundation
class Person: Codable {
let firstName: String
let lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
Usage #1 (encode a Person instance into a JSON string):
let person = Person(firstName: "John", lastName: "Doe")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // if necessary
let data = try! encoder.encode(person)
let jsonString = String(data: data, encoding: .utf8)!
print(jsonString)
/*
prints:
{
"firstName" : "John",
"lastName" : "Doe"
}
*/
Usage #2 (decode a JSON string into a Person instance):
let jsonString = """
{
"firstName" : "John",
"lastName" : "Doe"
}
"""
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
dump(person)
/*
prints:
â–¿ __lldb_expr_609.Person #0
- firstName: "John"
- lastName: "Doe"
*/
There is a Foundation class called NSJSONSerialization which can do conversion to and from JSON.
The method for converting from JSON to an object looks like this:
let jsonObject = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.MutableContainers,
error: &error) as NSDictionary
Note that the first argument to this method is the JSON data, but not as a string object, instead as a NSData object (which is how you'll often times get JSON data anyway).
You most likely will want a factory method for your class that takes JSON data as an argument, makes use of this method and returns an initialize object of your class.
To inverse this process and create JSON data out of an object, you'll want to make use of dataWithJSONObject, in which you'll pass an object that can be converted into JSON and have an NSData? returned. Again, you'll probably want to create a helper method that requires no arguments as an instance method of your class.
As far as I know, the easiest way to handle this is to create a way to map your objects properties into a dictionary and pass that dictionary for turning your object into JSON data. Then when turning your JSON data into the object, expect a dictionary to be returned and reverse the mapping process. There may be an easier way though.
You can achieve this by using ObjectMapper library. It'll give you more control on variable names and the values and the prepared JSON. After adding this library extend the Mappable class and define mapping(map: Map) function.
For example
class User: Mappable {
var id: Int?
var name: String?
required init?(_ map: Map) {
}
// Mapping code
func mapping(map: Map) {
name <- map["name"]
id <- map["id"]
}
}
Use it like below
let user = Mapper<User>().map(JSONString)
First, create a Swift object like this
struct Person {
var firstName: String?;
var lastName: String?;
init() {
}
}
After that, serialize your JSON data you retrieved, using the built-in NSJSONSerialization and parse the values into the Person object.
var person = Person();
var error: NSError?;
var response: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(), error: &error);
if let personDictionary = response as? NSDictionary {
person.firstName = personDictionary["firstName"] as? String;
person.lastName = personDictionary["lastName"] as? String;
}
UPDATE:
Also please take a look at those libraries
Swift-JsonSerialiser
ROJSONParser
Take a look at NSKeyValueCoding.h, specifically setValuesForKeysWithDictionary. Once you deserialize the json into a NSDictionary, you can then create and initialize your object with that dictionary instance, no need to manually set values on the object. This will give you an idea of how the deserialization could work with json, but you will soon find out you need more control over deserialization process. This is why I implement a category on NSObject which allows fully controlled NSObject initialization with a dictionary during json deserialization, it basically enriches the object even further than setValuesForKeysWithDictionary can do. I also have a protocol used by the json deserializer, which allows the object being deserialized to control certain aspects, for example, if deserializing an NSArray of objects, it will ask that object what is the type name of the objects stored in the array.