How to serialize or convert Swift objects to JSON?
Asked Answered
P

9

135

This below class

class User: NSManagedObject {
  @NSManaged var id: Int
  @NSManaged var name: String
}

Needs to be converted to

{
    "id" : 98,
    "name" : "Jon Doe"
}

I tried manually passing the object to a function which sets the variables into a dictionary and returns the dictionary. But I would want a better way to accomplish this.

Professorship answered 13/4, 2015 at 6:25 Comment(6)
Your best bet is to run through it and save it to arrays and dicts, then convert.Shurlocke
Refer this - github.com/dankogai/swift-jsonAndromeda
@Shurlocke could you point me to an example ? I don't know how to run through an object.Professorship
Well, you need to go through the object itself, take all the values, and add it to a dict manually, and repeat.Shurlocke
@Shurlocke I actually tried that. But for large objects compile time increases dramatically.Professorship
@redo1135 what are you even doing with your code? The compile time should not increase dramatically, I don't know what you're doing that increases compile time.Shurlocke
B
205

In Swift 4, you can inherit from the Codable type.

struct Dog: Codable {
    var name: String
    var owner: String
}

// Encode
let dog = Dog(name: "Rex", owner: "Etgar")

let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(dog)
let json = String(data: jsonData, encoding: String.Encoding.utf8)

// Decode
let jsonDecoder = JSONDecoder()
let secondDog = try jsonDecoder.decode(Dog.self, from: jsonData)
Biparty answered 14/6, 2017 at 18:12 Comment(10)
The encoding type should be .utf8 instead of .utf16Trunks
@ChanJingHong it depends on what you’re trying to encodeBiparty
For this specific example does .utf16 work? I tried but it doesn't work.Trunks
@ChanJingHong Strange, I tried it 3 months ago and it worked. I think I should have put the encoding type also as an argument in the decoding method. I’ll check it out.Biparty
What if you have a series objects of various types? You don't know much about the types except they are all Codable. Is there a way to handle this situation?Lafontaine
It would be nice if you mentioned jsonString to jsonData conversion step @BipartyStevens
Doesn't this assume you have control over the object's class definition? This often isn't the case.Derringdo
question was specifically about encoding class (what I need) not struct.Marital
@Marital this will work with both class and struct as wellDysgenics
@NishadArora classes behave differently than structs. f.e. you can't assign custom name to struct via @objc(MyName). I stand by my point, the question was about class not structMarital
L
55

Along with Swift 4 (Foundation) now it is natively supported in both ways, JSON string to an object - an object to JSON string. Please see Apple's documentation here JSONDecoder() and here JSONEncoder()

JSON String to Object

let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let myStruct = try! decoder.decode(myStruct.self, from: jsonData)

Swift Object to JSONString

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(myStruct)
print(String(data: data, encoding: .utf8)!)
Lamoreaux answered 7/10, 2017 at 20:47 Comment(1)
Link is broken..Gannon
P
28

UPDATE: Codable protocol introduced in Swift 4 should be sufficient for most of the JSON parsing cases. Below answer is for people who are stuck in previous versions of Swift and for legacy reasons

EVReflection :

  • This works of reflection principle. This takes less code and also supports NSDictionary, NSCoding, Printable, Hashable and Equatable

Example:

    class User: EVObject { # extend EVObject method for the class
       var id: Int = 0
       var name: String = ""
       var friends: [User]? = []
    }

    # use like below
    let json:String = "{\"id\": 24, \"name\": \"Bob Jefferson\", \"friends\": [{\"id\": 29, \"name\": \"Jen Jackson\"}]}"
    let user = User(json: json)

ObjectMapper :

  • Another way is by using ObjectMapper. This gives more control but also takes a lot more code.

Example:

    class User: Mappable { # extend Mappable method for the class
       var id: Int?
       var name: String?

       required init?(_ map: Map) {

       }

       func mapping(map: Map) { # write mapping code
          name    <- map["name"]
          id      <- map["id"]
       }

    }

    # use like below
    let json:String = "{\"id\": 24, \"name\": \"Bob Jefferson\", \"friends\": [{\"id\": 29, \"name\": \"Jen Jackson\"}]}"
    let user = Mapper<User>().map(json)
Professorship answered 14/4, 2015 at 6:23 Comment(6)
Does it supports mapping images also??Underworld
@Underworld sorry I'm not sure if that's possible.Professorship
@ Suresh: It is possible by writing custom transform as shown examples. I converted the image to String and then added in Json object. It helps a lot specially working with watch osUnderworld
Hi, do you know how to initialize a Mappable class and set properties manually and then convert object to jsonString?Vertu
what is the impact of codeable protocol from swift 4/ ios 11?? . Can we convert NSManagedObject to JSON in swift 4 using this??Smackdab
If you are using ObjectMapper, you can reduce a lot of boilerplate code by using [BetterMappable][1] which is written over ObjectMapper using PropertyWrappers. You need to be on Swift 5.1 to use it. [1]: github.com/PhonePe/BetterMappableParental
C
15

I worked a bit on a smaller solution that doesn't require inheritance. But it hasn't been tested much. It's pretty ugly atm.

https://github.com/peheje/JsonSerializerSwift

You can pass it into a playground to test it. E.g. following class structure:

//Test nonsense data
class Nutrient {
    var name = "VitaminD"
    var amountUg = 4.2

    var intArray = [1, 5, 9]
    var stringArray = ["nutrients", "are", "important"]
}

class Fruit {
    var name: String = "Apple"
    var color: String? = nil
    var weight: Double = 2.1
    var diameter: Float = 4.3
    var radius: Double? = nil
    var isDelicious: Bool = true
    var isRound: Bool? = nil
    var nullString: String? = nil
    var date = NSDate()

    var optionalIntArray: Array<Int?> = [1, 5, 3, 4, nil, 6]
    var doubleArray: Array<Double?> = [nil, 2.2, 3.3, 4.4]
    var stringArray: Array<String> = ["one", "two", "three", "four"]
    var optionalArray: Array<Int> = [2, 4, 1]

    var nutrient = Nutrient()
}

var fruit = Fruit()
var json = JSONSerializer.toJson(fruit)

print(json)

prints

{"name": "Apple", "color": null, "weight": 2.1, "diameter": 4.3, "radius": null, "isDelicious": true, "isRound": null, "nullString": null, "date": "2015-06-19 22:39:20 +0000", "optionalIntArray": [1, 5, 3, 4, null, 6], "doubleArray": [null, 2.2, 3.3, 4.4], "stringArray": ["one", "two", "three", "four"], "optionalArray": [2, 4, 1], "nutrient": {"name": "VitaminD", "amountUg": 4.2, "intArray": [1, 5, 9], "stringArray": ["nutrients", "are", "important"]}}
Carouse answered 19/6, 2015 at 22:52 Comment(2)
can you please provide its swift 2.3 version?Incommensurable
Looks awesome, but it doesn't work with SPM...Champerty
H
10

This is not a perfect/automatic solution but I believe this is the idiomatic and native way to do such. This way you don't need any libraries or such.

Create an protocol such as:

/// A generic protocol for creating objects which can be converted to JSON
protocol JSONSerializable {
    private var dict: [String: Any] { get }
}

extension JSONSerializable {
    /// Converts a JSONSerializable conforming class to a JSON object.
    func json() rethrows -> Data {
        try JSONSerialization.data(withJSONObject: self.dict, options: nil)
    }
}

Then implement it in your class such as:

class User: JSONSerializable {
    var id: Int
    var name: String

    var dict { return ["id": self.id, "name": self.name]  }
}

Now:

let user = User(...)
let json = user.json()

Note: if you want json as a string, it is very simply to convert to a string: String(data: json, encoding .utf8)

Hoarding answered 4/4, 2017 at 4:47 Comment(0)
C
8

Some of the above answers are completely fine, but I added an extension here, just to make it much more readable and usable.

extension Encodable {
    var convertToString: String? {
        let jsonEncoder = JSONEncoder()
        jsonEncoder.outputFormatting = .prettyPrinted
        do {
            let jsonData = try jsonEncoder.encode(self)
            return String(data: jsonData, encoding: .utf8)
        } catch {
            return nil
        }
    }
}

struct User: Codable {
     var id: Int
     var name: String
}

let user = User(id: 1, name: "name")
print(user.convertToString!)

//This will print like the following:

{
  "id" : 1,
  "name" : "name"
}
Careerism answered 21/2, 2019 at 21:3 Comment(0)
I
4
struct User:Codable{
 var id:String?
 var name:String?
 init(_ id:String,_ name:String){
   self.id  = id
   self.name = name
 }
}

Now just make your object like this

let user = User("1","pawan")

do{
      let userJson =  try JSONEncoder().encode(parentMessage) 
            
    }catch{
         fatalError("Unable To Convert in Json")      
    }

Then reconvert from json to Object

let jsonDecoder = JSONDecoder()
do{
   let convertedUser = try jsonDecoder.decode(User.self, from: userJson.data(using: .utf8)!)
 }catch{
   
 }
Indigotin answered 11/1, 2019 at 6:28 Comment(0)
E
4

2021 | SWIFT 5.1 | Results solution

Data sample:

struct ConfigCreds: Codable { // Codable is important!
    // some params
}

Solution usage sample:

var configCreds = ConfigCreds()
var jsonStr: String = ""

// Object -> JSON
configCreds
   .asJson()
   .onSuccess { jsonStr = $0 }
   .onFailure { _ in // any failure code }

// JSON -> object of type "ConfigCreds"
someJsonString
    .decodeFromJson(type: ConfigCreds.self)
    .onSuccess { configCreds = $0 }
    .onFailure { _ in // any failure code }

Back-End code:

@available(macOS 10.15, *)
public extension Encodable {
    func asJson() -> Result<String, Error>{
        JSONEncoder()
            .try(self)
            .flatMap{ $0.asString() }
    }
}

public extension String {
    func decodeFromJson<T>(type: T.Type) -> Result<T, Error> where T: Decodable {
        self.asData()
            .flatMap { JSONDecoder().try(type, from: $0) }
    }
}

///////////////////////////////
/// HELPERS
//////////////////////////////

@available(macOS 10.15, *)
fileprivate extension JSONEncoder {
    func `try`<T : Encodable>(_ value: T) -> Result<Output, Error> {
        do {
            return .success(try self.encode(value))
        } catch {
            return .failure(error)
        }
    }
}

fileprivate extension JSONDecoder {
    func `try`<T: Decodable>(_ t: T.Type, from data: Data) -> Result<T,Error> {
        do {
            return .success(try self.decode(t, from: data))
        } catch {
            return .failure(error)
        }
    }
}

fileprivate extension String {
    func asData() -> Result<Data, Error> {
        if let data = self.data(using: .utf8) {
            return .success(data)
        } else {
            return .failure(WTF("can't convert string to data: \(self)"))
        }
    }
}

fileprivate extension Data {
    func asString() -> Result<String, Error> {
        if let str = String(data: self, encoding: .utf8) {
            return .success(str)
        } else {
            return .failure(WTF("can't convert Data to string"))
        }
    }
}

fileprivate func WTF(_ msg: String, code: Int = 0) -> Error {
    NSError(code: code, message: msg)
}

internal extension NSError {
    convenience init(code: Int, message: String) {
        let userInfo: [String: String] = [NSLocalizedDescriptionKey:message]
        self.init(domain: "FTW", code: code, userInfo: userInfo)
    }
}
Eisinger answered 14/9, 2021 at 18:18 Comment(2)
Not sure how to use this. IDE errors with "Value of type 'JSONEncoder.Output' has no member 'asString'" and "Value of type 'String' has no member 'asData'"Eduardo
@Eduardo by the way, I have fixed the code hereEisinger
R
2

Not sure if lib/framework exists, but if you would like to do it automatically and you would like to avoid manual labour :-) stick with MirrorType ...

class U {

  var id: Int
  var name: String

  init(id: Int, name: String) {
    self.id = id
    self.name = name
  }

}

extension U {

  func JSONDictionary() -> Dictionary<String, Any> {
    var dict = Dictionary<String, Any>()

    let mirror = reflect(self)

    var i: Int
    for i = 0 ; i < mirror.count ; i++ {
      let (childName, childMirror) = mirror[i]

      // Just an example how to check type
      if childMirror.valueType is String.Type {
        dict[childName] = childMirror.value
      } else if childMirror.valueType is Int.Type {
        // Convert to NSNumber for example
        dict[childName] = childMirror.value
      }
    }

    return dict
  }

}

Take it as a rough example, lacks proper conversion support, lacks recursion, ... It's just MirrorType demonstration ...

P.S. Here it's done in U, but you're going to enhance NSManagedObject and then you'll be able to convert all NSManagedObject subclasses. No need to implement this in all subclasses/managed objects.

Riddance answered 13/4, 2015 at 6:54 Comment(2)
Hi, I'm trying this method, but every time I'm getting 0 as the count. Could you be more specific as to what I should do ? I think @NSManaged is causing problems. How do I enhance NSManagedObject ?Professorship
Didn't try with @NSManaged. Maybe it's causing your problem. In this case, I would write it in Objective-C and then will use it from Swift.Riddance

© 2022 - 2024 — McMap. All rights reserved.