Swift Tree Structure, encoding and accessing nodes - json

I am having trouble figuring out why the following does not run. The goal is to create a tree structure with reference types that can be saved to a json file, but the following playground code does not run.
The root node has a nil parent, but I thought the encoder ignored nil values. In my app I get an EXC_BAD_ACCESS. Does this need to be done with structs instead of classes, and if so, is there a way to access a particular node without traversing the entire tree? Any help appreciated.
import Cocoa
final class Node: Codable {
var id: UUID
var data: [MyData]
var children: [Node]
var parent: Node? = nil
init() {
self.id = UUID()
self.data = []
self.children = []
}
func add(data: MyData) {
data.parent = self
self.data.append(data)
}
func add(child: Node) {
child.parent = self
self.children.append(child)
}
}
final class MyData: Codable {
var id: UUID
var label: String
var value: String
var parent: Node? = nil
init(label: String, value: String) {
self.id = UUID()
self.label = label
self.value = value
}
}
var root = Node()
root.add(data: MyData(label: "label 1", value: "value 1"))
root.add(data: MyData(label: "label 2", value: "value 2"))
var child = Node()
child.add(data: MyData(label: "label 3", value: "value 3"))
root.add(child: child)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try encoder.encode(root)
print(String(data: json, encoding: .utf8)!)

The problem is that by default Codable tries to encode/decode all properties of a conforming type. This means that the children try to encode/decode their parent, which parent contains the children as well, hence leading to an infinite loop.
You need to manually specify which properties to encode/decode by providing a CodingKey conformant type. By leaving out the parent property from both Node.CodingKeys and MyData.CodingKeys, you resolve the infinite loop.
import Foundation
final class Node: Codable {
let id = UUID()
var data: [MyData]
var children: [Node]
weak var parent: Node? = nil
init() {
self.data = []
self.children = []
}
func add(data: MyData) {
data.parent = self
self.data.append(data)
}
func add(child: Node) {
child.parent = self
self.children.append(child)
}
private enum CodingKeys: String, CodingKey {
case data
case children
}
}
final class MyData: Codable {
let id = UUID()
var label: String
var value: String
weak var parent: Node? = nil
init(label: String, value: String) {
self.label = label
self.value = value
}
private enum CodingKeys: String, CodingKey {
case label
case value
}
}
var root = Node()
root.add(data: MyData(label: "label 1", value: "value 1"))
root.add(data: MyData(label: "label 2", value: "value 2"))
var child = Node()
child.add(data: MyData(label: "label 3", value: "value 3"))
root.add(child: child)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try encoder.encode(root)
print(String(data: json, encoding: .utf8)!)

Related

How convert data to Json in Realm version 10.15.0

in the realm version 10.7.1 it used this code but when migrating to 10.15.0 it is no longer possible because the ListBase class no longer exists
extension Object {
func toWebServiceRequest() -> [String:AnyObject] {
let properties = self.objectSchema.properties.map { $0.name }
var dicProps = [String:AnyObject]()
for (key, value) in self.dictionaryWithValues(forKeys: properties) {
if let value = value as? ListBase {
dicProps[key] = value.toArray() as AnyObject
} else if let value = value as? Object {
dicProps[key] = value.toWebServiceRequest() as AnyObject
} else {
dicProps[key] = value as AnyObject
}
}
return dicProps
}
}
extension ListBase {
func toArray() -> [AnyObject] {
var _toArray = [AnyObject]()
for i in 0..<self._rlmArray.count {
let obj = unsafeBitCast(self._rlmArray[i], to: Object.self)
_toArray.append(obj.toWebServiceRequest() as AnyObject)
}
return _toArray
}
}
The mongo team wants you to use the Codable protocol rather than some custom encoding mechanism. This is really the proper way to do it, otherwise you have to rely on Realm implementation details.
class Parent: Object, Codable {
#Persisted var name: String = ""
#Persisted var children: List<Child>
convenience init(name: String, children: [Child] = []) {
self.init()
self.name = name
self.children.append(objectsIn: children)
}
}
If the server is expecting keys different from your object's property names you can define custom CodingKeys:
class Child: Object, Codable {
#Persisted var id: Int = 0
#Persisted var name: String = ""
enum CodingKeys: String, CodingKey {
case id = "_id"
case name = "name"
}
convenience init(id: Int, name: String) {
self.init()
self.id = id
self.name = name
}
}
Usage is simple:
func makeParent() -> Parent {
Parent(name: "Alex", children: [
Child(id: 1, name: "Jim"),
Child(id: 2, name: "Tom"),
Child(id: 3, name: "Sam"),
Child(id: 4, name: "Joe"),
])
}
func encode<Item: Encodable>(_ item: Item) throws -> Data {
let encoder = JSONEncoder()
return try encoder.encode(item)
}
func convertDataToString(_ data: Data) throws -> String? {
return String(data: data, encoding: .utf8)
}
func decode<Item: Decodable>(_ type: Item.Type, from data: Data) throws -> Item {
let decoder = JSONDecoder()
return try decoder.decode(type, from: data)
}
func executeProgram() throws {
let parent = makeParent()
let encodedJSONData = try encode(parent)
guard let encodedJSONString = try convertDataToString(encodedJSONData) else {
throw Error.badJSON
}
print("JSON: \(encodedJSONString)")
let decodedParent = try decode(Parent.self, from: encodedJSONData)
print("Decoded Parent: \(decodedParent)")
}
On the encode route this results in:
JSON:
{"name":"Alex","children":[{"_id":1,"name":"Jim"},{"_id":2,"name":"Tom"},{"_id":3,"name":"Sam"},{"_id":4,"name":"Joe"}]}
And on the decode route this results in:
Decoded Parent: Parent { name = Alex; children = List
<0x600000a2b800> ( [0] Child { id = 1; name = Jim; }, [1]
Child { id = 2; name = Tom; }, [2] Child { id = 3;
name = Sam; }, [3] Child { id = 4; name = Joe; } ); }
You can obviously omit the String conversion just work with the resulting Data.

How do I access integer value in JSON API?

I'm trying to retrieve an integer value from a JSON file in swift. I'm doing this as follows: self.trip.dist = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].dist) but I'm getting this error error.
Here is a link to the JSON file that I'm accessing. I'm trying to access the dist value.
These are my structures:
struct JSONStructure: Decodable {
var Trip: [TripStructure]
}
struct TripStructure: Decodable {
var LegList: LegListStructure
}
struct LegListStructure: Decodable {
var Leg: [LegStructure]
}
struct LegStructure: Decodable {
var Origin: StationStructure
var Destination: StationStructure
var Product: ProductStructure
var name: String
var type: String
var dist: Int
}
struct StationStructure: Decodable {
var time: String
var name: String
var date: String
}
struct ProductStructure: Decodable {
var catIn: String
}
// Just to condense my varibales
struct LocationInfo {
var iD = String()
var input = String()
var lat = String()
var lon = String()
var name = String()
var time = String()
var date = String()
var vehicleType = String()
var transportType = String()
var dist = String()
var legName = String()
}
Here is the function I'm using to call the function:
#Published var trip: LocationInfo = LocationInfo()
#Published var dest: LocationInfo = LocationInfo()
#Published var origin: LocationInfo = LocationInfo()
#Published var arrivalTime = String()
#Published var travelDate = String()
#Published var searchForArrival = String()
#Published var tripIndex = Int()
#Published var Trips: [Dictionary<String, String>] = []
public func FetchTrip() {
Trips.removeAll()
let tripKey = "40892db48b394d3a86b2439f9f3800fd"
let tripUrl = URL(string: "http://api.sl.se/api2/TravelplannerV3_1/trip.json?key=\(tripKey)&originExtId=\(self.origin.iD)&destExtId=\(self.dest.iD)&Date=\(self.travelDate)&Time=\(self.arrivalTime)&searchForArrival=\(self.searchForArrival)")
URLSession.shared.dataTask(with: tripUrl!) {data, response, error in
if let data = data {
do {
let decodedJson = try JSONDecoder().decode(JSONStructure.self, from: data)
self.tripIndex = decodedJson.Trip.count - 1
for i in 0..<decodedJson.Trip[self.tripIndex].LegList.Leg.count {
self.trip.transportType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].type
if self.trip.transportType == "WALK" {
self.origin.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.name
self.origin.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.time.prefix(5))
self.origin.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.date
self.dest.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.name
self.dest.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.time.prefix(5))
self.dest.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.date
self.trip.vehicleType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Product.catIn
self.trip.dist = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].dist) // This is where the problem lies
self.Trips.append(["Origin": self.origin.name, "Destination": self.dest.name, "OriginTime": self.origin.time, "DestTime": self.dest.time, "OriginDate": self.origin.date, "DestDate": self.dest.date, "TransportType": self.trip.transportType, "VehicleType": self.trip.vehicleType, "Distance": self.trip.dist])
}
else {
self.origin.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.name
self.origin.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.time.prefix(5))
self.origin.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Origin.date
self.dest.name = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.name
self.dest.time = String(decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.time.prefix(5))
self.dest.date = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Destination.date
self.trip.vehicleType = decodedJson.Trip[self.tripIndex].LegList.Leg[i].Product.catIn
self.trip.legName = decodedJson.Trip[self.tripIndex].LegList.Leg[i].name
self.Trips.append(["Origin": self.origin.name, "Destination": self.dest.name, "OriginTime": self.origin.time, "DestTime": self.dest.time, "OriginDate": self.origin.date, "DestDate": self.dest.date, "TransportType": self.trip.transportType, "VehicleType": self.trip.vehicleType, "LegName": self.trip.legName])
}
}
} catch {
print(error)
}
}
}.resume()
}
I am getting this error in the console:
keyNotFound(CodingKeys(stringValue: "dist", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "Trip", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "LegList", intValue: nil), CodingKeys(stringValue: "Leg", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dist\", intValue: nil) (\"dist\").", underlyingError: nil))
This site is great: https://app.quicktype.io You just paste in your json and it will write the Codable for you. QED.
You need to use an Optional Int to extract the dist values because not all legs have one.
Here's the minimum necessary to access the dist values:
func example() {
let url = URL(string: "https://api.sl.se/api2/TravelplannerV3_1/trip.json?key=40892db48b394d3a86b2439f9f3800fd&originExtId=300101416&destExtId=300101426&Date=2021-04-15&Time=08:00&searchForArrival=1")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let travelplanner = try? JSONDecoder().decode(Travelplanner.self, from: data)
print(travelplanner as Any)
}
}
.resume()
}
struct Travelplanner: Codable {
let trip: [Trip]
enum CodingKeys: String, CodingKey {
case trip = "Trip"
}
}
struct Trip: Codable {
let legList: LegList
enum CodingKeys: String, CodingKey {
case legList = "LegList"
}
}
struct LegList: Codable {
let leg: [Leg]
enum CodingKeys: String, CodingKey {
case leg = "Leg"
}
}
struct Leg: Codable {
let origin: Station
let destination: Station
let product: Product?
let name: String
let type: String
let dist: Int?
enum CodingKeys: String, CodingKey {
case origin = "Origin"
case destination = "Destination"
case product = "Product"
case name
case type
case dist
}
}
struct Station: Codable {
let name: String
let time: String
let date: String
}
struct Product: Codable {
let catIn: String
}
I'm glad you found a solution. There are other issues with your code that I feel the need to address.
The following code has the same effect but is, IMO, much cleaner:
func fetchTrip() {
let url = URL(string: "https://api.sl.se/api2/TravelplannerV3_1/trip.json?key=40892db48b394d3a86b2439f9f3800fd&originExtId=300101416&destExtId=300101426&Date=2021-04-15&Time=08:00&searchForArrival=1")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let decodedJson = try! JSONDecoder().decode(Travelplanner.self, from: data)
let lastLeg = decodedJson.trip.last?.legList.leg.last
self.tripIndex = decodedJson.trip.count - 1
self.trip.transportType = lastLeg?.type ?? ""
self.origin.name = lastLeg?.origin.name ?? ""
self.origin.time = lastLeg.map { String($0.origin.time.prefix(5)) } ?? ""
self.origin.date = lastLeg?.origin.date ?? ""
self.dest.name = lastLeg?.destination.name ?? ""
self.dest.time = lastLeg.map { String($0.destination.time.prefix(5)) } ?? ""
self.dest.date = lastLeg?.destination.date ?? ""
self.trip.vehicleType = lastLeg?.product?.catIn ?? ""
if let lastWalkLeg = decodedJson.trip.last?.legList.leg.last(where: { $0.type == "WALK" }) {
self.trip.dist = String(lastWalkLeg.dist ?? 0)
}
if let lastNonwalkLeg = decodedJson.trip.last?.legList.leg.last(where: { $0.type != "WALK" }) {
self.trip.legName = lastNonwalkLeg.name
}
self.trips = (decodedJson.trip.last?.legList.leg ?? [])
.map { $0.asDict }
}
}
.resume()
}
}
extension Leg {
var asDict: [String: String] {
[
"Origin": origin.name,
"Destination": destination.name,
"OriginTime": String(origin.time.prefix(5)),
"DestTime": String(destination.time.prefix(5)),
"OriginDate": origin.date,
"DestDate": destination.date,
"TransportType": type,
"VehicleType": product?.catIn ?? "",
].merging(type == "WALK" ? ["Distance": String(dist ?? 0)] : ["LegName": name], uniquingKeysWith: { $1 })
}
}
For most of the variables, you are assigning to the same var each time through the loop. This is just wasteful. Assign the values from the last element of the loop and be done with it.
Most of the work being done in the if...else... blocks are identical. Separate out the identical parts and only put the parts that care about the Leg.type in the if/else block. Do you actually use all the class properties in other methods of the class or were you just using them to make local variables in a weird way? (I'm assuming the former in the above code just in case.) If you don't need all the other properties in other methods, and the only point is to populate the trips array, you could unload most of the code (except the self.trips =... line of course.

Return Children and Siblings values in response

It's probably a trivial question, but I couldn't solve it and didn't find the answer.
I have 3 tables related to each other as Parent-Child and Siblings. And I want to return some values form all of the tables and the same JSON response.
final class ClimbingGym: Model, Content {
static let schema = "climbing_gyms"
#ID(key: .id)
var id: UUID?
#Field(key: "name")
var name: String
#Field(key: "description")
var description: String
#Siblings(through: ClimbingDisciplineClimbingGym.self, from: \.$gym, to: \.$discipline)
var disciplines: [ClimbingDiscipline]
#Children(for: \.$gym)
var socialNetworks: [SocialNetwork]
init() { }
init(
id: UUID? = nil,
name: String,
description: String,
) {
self.id = id
self.name = name
self.description = description
}
}
enum ClimbingDisciplineType: String, Codable, CaseIterable, Equatable {
static let schema = "climbing_discipline_type"
case lead
case boulder
case speed
}
final class ClimbingDiscipline: Model, Content {
static let schema = "climbing_disciplines"
#ID(key: .id)
var id: UUID?
#Enum(key: "type")
var type: ClimbingDisciplineType
init() { }
init(
id: UUID? = nil,
type: ClimbingDisciplineType
) {
self.id = id
self.type = type
}
}
final class SocialNetwork {
static let schema = "social_networks"
#ID(key: .id)
var id: UUID?
#Field(key: "link")
var link: String
#Parent(key: "gym_id")
var gym: ClimbingGym
init() { }
init(
id: UUID? = nil,
link: String,
gym: ClimbingGym
) throws {
self.id = id
self.link = link
self.$gym.id = try gym.requireID()
}
}
And I want to return that model:
struct ClimbingGymResponse: Codable, Content {
let id: UUID
let name: String
let description: String
let disciplines: [ClimbingDisciplineType]
let socialNetworks: [String]
}
so the query that I'm using now looks like that
func getAll(req: Request) throws -> EventLoopFuture<[ClimbingGymResponse]> {
ClimbingGym
.query(on: req.db)
.join(children: \ClimbingGym.$socialNetworks)
.join(siblings: \ClimbingGym.$disciplines)
.all()
}
and it obviously doesn't work, because it returns [ClimbingGym] instead of [ClimbingGymResponse].
So how can I transform one to another?
I have problems with filling disciplines: [ClimbingDisciplineType] and socialNetworks: [String] field for each gym
Thank you!
You can map the array of results into the type you want. So
ClimbingGym
.query(on: req.db)
.with(\.$socialNetworks)
.with(\.$disciplines)
.all().flatMapThrowing { gyms in
try gyms.map { try ClimbingGymResponse(id: $0.requireID(), ...) }
}

Swift Failable Initializer with SwiftyJSON

I’m trying to initialise a simple data model object with some JSON from SwiftyJSON. I’d like the initialiser to fail and return nil if any of the required JSON values aren’t present. Here’s my code:
class ProductCategory: NSObject {
let id: String
let sortOrder: Int
let name: String
let imageURL: String
let ranges: [String]
init?(json: JSON) {
if let jsonID = json["id"].string,
jsonSortOrder = json["sortOrder"].int,
jsonName = json["name"].string,
jsonImageURL = json["imageURL"].string {
id = jsonID
sortOrder = jsonSortOrder
name = jsonName
imageURL = jsonImageURL
ranges = json["ranges"].arrayValue.map { $0.string! }
} else {
return nil
}
}
}
I’d expect this to work. In the event that we didn’t hit all those json values, simply return nil and bail out. However, I get an error on the return nil, stating:
All stored properties of a class instance must be initialized before
returning nil from an initializer.
I’m confused: isn’t the point of a failable initializer that I can bail out without setting it up if something goes wrong? The object returned would be nil, why would there be any value in setting up its properties?
So here’s what I ended up doing – Greg was right but I ended up switching to a struct as a result:
struct ProductCategory {
let id: String
let sortOrder: Int
let name: String
let imageURL: String
let ranges: [String]
init?(json: JSON) {
guard let jsonID = json["id"].string,
let jsonSortOrder = json["sortOrder"].int,
let jsonName = json["name"].string,
let jsonImageURL = json["image"].string else {
return nil
}
self.id = jsonID
self.sortOrder = jsonSortOrder
self.name = jsonName
self.imageURL = jsonImageURL
self.ranges = json["ranges"].arrayValue.map { $0.string! }
}
}
Failable Initializers for Classes:
"For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place."
So
init?(json: JSON) {
self.id = json["id"].string
self.sortOrder = json["sortOrder"].int
...
if ... { return nil }
}

How to serialize or convert Swift objects to JSON?

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.
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)
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)!)
You can find all details and examples here Ultimate Guide to JSON Parsing With Swift 4
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)
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"]}}
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)
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"
}
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.
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{
}
2021 | SWIFT 5.1 | Results solution
Input data:
struct ConfigCreds: Codable {
// some params
}
usage:
// get JSON from Object
configCreds
.asJson()
.onSuccess{ varToSaveJson = $0 }
.onFailure{ _ in // any failure code }
// get object of type "ConfigCreds" from JSON
someJsonString
.decodeFromJson(type: ConfigCreds.self)
.onSuccess { configCreds = $0 }
.onFailure{ _ in // any failure code }
Back 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)
}