Recursive function to replace json objects - swift

I have this json file:
[
{
"person": {
"#id": "value1",
"name": "Mattia"
},
"person1": {
"#ref": "value1"
},
"subPersons": [
{
"#id": "value2",
"name": "Luca",
"key": {
"#ref": "value1"
}
},
{
"#ref": "value1"
},
{
"#id": "value3",
"subsubPersons": [
{
"again": {
"#ref": "value2"
}
}
]
}
],
"key": {
"subKey": {
"#ref": "value1"
}
}
}
]
I need to map all objects that contains a #id so replace all #ref values with related #id values mapped. I'd like to obtain this:
[
{
"person": {
"#id": "value1",
"name": "Mattia"
},
"person1": {
"#id": "value1",
"name": "Mattia"
},
"subPersons": [
{
"#id": "value2",
"name": "Luca",
"key": {
"#id": "value1",
"name": "Mattia"
}
},
{
"#id": "value1",
"name": "Mattia"
},
{
"#id": "value3",
"subsubPersons": [
{
"again": {
"#id": "value2",
"name": "Luca",
"key": {
"#id": "value1",
"name": "Mattia"
}
}
}
]
}
],
"key": {
"subKey": {
"#id": "value1",
"name": "Mattia"
}
}
}
]
I'm using this class to replace values:
import UIKit
import Alamofire
import AlamofireObjectMapper
import ObjectMapper
import SwiftyJSON
import SwiftDate
import Async
class FindAndReplace {
var ids = Dictionary<String, JSON>()
var dictChanged = Dictionary<String, JSON>()
var isDictInit: Bool = false
/*
* Find and Replace
*/
func findAndReplace (json: JSON) -> JSON {
findJSOGids(json)
let replaced = replaceJSOGrefs(json, ids: ids)
return replaced
}
/*
* Find "#id" keys and map values related
*/
func findJSOGids (value: JSON) {
for (key, subJson): (String, JSON) in value {
if (key == "#id") {
let mValueForKey = value[key].stringValue
ids[mValueForKey] = value
}
if (subJson.type == Type.Dictionary || subJson.type == Type.Array) {
findJSOGids(subJson)
}
}
}
/*
* Replace "#ref" keys with fields mapped in ids
*/
func replaceJSOGrefs (var value: JSON, var ids: Dictionary<String, JSON>) -> JSON {
if (value.type == Type.Dictionary) {
var result = Dictionary<String, JSON> ()
for (key, subJson): (String, JSON) in value {
if (key == "#ref") {
let mValueForKey = value[key].stringValue
var isReplaced = false
while (isReplaced == false) {
for (idKey, _): (String, JSON) in ids[mValueForKey]! {
if (idKey == "#ref") {
print("found a #ref in dictionary")
let dictValueReplaced = replaceJSOGrefs(ids[mValueForKey]!, ids: ids)
ids.updateValue(dictValueReplaced, forKey: mValueForKey)
}
}
}
return ids[mValueForKey]!
} else {
result[key] = replaceJSOGrefs(subJson, ids: ids)
}
}
return JSON(result)
} else if (value.type == Type.Array) {
var result = [JSON]()
for (_, subJson): (String, JSON) in value {
result.append(replaceJSOGrefs(subJson, ids: ids))
}
return JSON(result)
} else {
return value
}
}
}
It works but it misses some #ref values.
Can someone please help me?
Thanks in advance.
Edit
I'm using ObjectMapper to map objects.

I think that find-replace approach won't be as efficient since you'll have to do many passes on your data (until you can't find any #ref strings).
You should probably leverage the fact that your JSON models reference types semantics (as oppose to value types) and parse it as such, keeping #ref in the parsed objects as faulted references. Every object you parse you should add in the cache that can be referenced by #id. Then in the second pass you'll go through your cache rewiring each reference to using the cache you just built as a lookup table.
If every model implements following protocol
protocol RefObject {
func updateReferences(using cache: [String: RefObject])
}
you can implement it per-model to have a custom rewiring logic per each model class. Here are few examples of such model classes:
For a wildcard represented by just {"#ref": "xxx"} in JSON I'd create a pointer class that would simply point to the referred object.
class Pointer: RefObject {
let referredId: String
var referred: RefObject!
init(referedId: String) {
self.referredId = referredId
}
func updateReferences(using cache: [String : RefObject]) {
self.referred = cache[referredId]
}
}
For a person you can implement something similar to
class Person: RefObject {
let id: String
let name: String
var otherId: String?
var other: Person?
init(id: String, name: String, otherId: String?) {
self.id = id
self.name = name
self.otherId = otherId
}
func updateReferences(using cache: [String : RefObject]) {
other = otherId.flatMap{ cache[$0] as? Person }
}
}
(this assumes that person can have {"id": "xx", "name": "xx", "other": {"#ref": "xx"}} where "other" is other is optional
This is a general approach and not a particular implementation, but it would be very domain specific depending on your needs.
Update there is a similar protocol called JSON API (misleading name IMO, but it utilizes the same approach of referencing JSON objects by id). Here is an implementation in Swift: https://github.com/wvteijlingen/spine it might be worth checking it out

Related

Swift json decoder with mixed array

Looking for help to decode irregular (for a lack of a better word) json. As an example:
[
{
"texts":
[
{
"value": "value 1"
}
],
"commentType": "someComment"
},
{
"texts":
[
{
"value": "value 2"
}
],
"commentType": "someComment2"
},
{
"texts":
[
{
"evidences":
[
{
"evidenceCode": "code 1",
},
{
"evidenceCode": "code 2",
},
{
"evidenceCode": "code 3",
},
{
"evidenceCode": "code 4",
}
],
"value": "value 3"
}
],
"commentType": "someComment3"
}
]
I can decode comment and the first two "texts":
enum CodingKeys: String, CodingKey {
case texts
case commentType
}
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .commentType)
if let texts = try container.decodeIfPresent([[String: String]].self, forKey: .texts) {
for text in texts {
if let value = text["value"] {
// add to model object
}
}
}
} catch {
print(error)
}
But I get an error for the third "texts" block:
"Expected to decode String but found an array instead."
Which I understand, since now instead of an array of [String:String], it is a mixed array of [String:[String:String] and [String:String].
How do I decode value3 from that third block?
Actually this is not complicated at all because the content of texts is the same if we treat evidences as an optional array. The below models will decode the json correctly without any custom code.
struct Result: Decodable {
var texts: [TextData]
var commentType: String
}
struct TextData: Decodable {
let evidences: [Evidence]?
let value: String
}
struct Evidence: Decodable {
let evidenceCode: String
}

How can i parse an Json array of a list of different object using Codable?

I have an json array with a list of item, that has different property.
{
"items": [
{
"id": "1",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "2",
"name": "name",
"propertyOfA": "12",
"productype": "a"
},
{
"id": "3",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "1",
"name": "name",
"propertyOfB": "1243",
"productype": "b"
},
{
"id": "1",
"name": "name",
"propertyOfC": "1243",
"propertyOfC2": "1243",
"productype": "c"
}
]
}
What i usually do is parse it into something like :
struct RequestData: Codable {
let items: [Item]
}
enum ProductType: String, Codable {
case a = "A"
case b = "B"
case c = "C"
}
struct Item: Codable {
let id, name: String
let propertyOfA: String?
let productype: String
let propertyOfB: String?
let propertyOfC: String?
let propertyOfC2: String?
}
But if this array keep growing, its gonna end up with a Item with a huge amount of optional property,
so i want each object have its own dedicated class
adding some kind of bridge inside codable parsings,
what i want to archive is something like :
struct RequestData: Codable {
let items: Items
}
struct items: Codable {
let arrayA: [A]
let arrayB = [B]
let arrayC = [C]
}
struct A: Codable {
let id: String,
let name: String,
let propertyOfA: String,
let producttype: Productype
}
struct B {
...
}
struct C {
...
}
can i do this with Codable ?
A reasonable solution is an enum with associated values because the type can be determined by the productype key. The init method first decodes the productype with a CodingKey then in a switch it decodes (from a singleValueContainer) and associates the proper type/value to the corresponding case.
enum ProductType: String, Codable {
case a, b, c
}
struct Root : Codable {
let items : [Product]
}
struct ProductA : Codable {
let id, name: String
let productype: ProductType
let propertyOfA : String
}
struct ProductB : Codable {
let id, name: String
let productype: ProductType
let propertyOfB : String
}
struct ProductC : Codable {
let id, name: String
let productype: ProductType
let propertyOfC, propertyOfC2 : String
}
enum Product : Codable {
case a(ProductA), b(ProductB), c(ProductC)
enum CodingKeys : String, CodingKey { case productype }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ProductType.self, forKey: .productype)
let singleContainer = try decoder.singleValueContainer()
switch type {
case .a : self = .a(try singleContainer.decode(ProductA.self))
case .b : self = .b(try singleContainer.decode(ProductB.self))
case .c : self = .c(try singleContainer.decode(ProductC.self))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let productA): try container.encode(productA)
case .b(let productB): try container.encode(productB)
case .c(let productC): try container.encode(productC)
}
}
}
And decode
let jsonString = """
{
"items": [
{
"id": "1",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "2",
"name": "name",
"propertyOfA": "12",
"productype": "a"
},
{
"id": "3",
"name": "name",
"propertyOfA": "1243",
"productype": "a"
},
{
"id": "1",
"name": "name",
"propertyOfB": "1243",
"productype": "b"
},
{
"id": "1",
"name": "name",
"propertyOfC": "1243",
"propertyOfC2": "1243",
"productype": "c"
}
]
}
"""
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch { print(error)}
To read the enum values use also a switch.

How can i sort multidimension array with specific name in swift? [duplicate]

This question already has answers here:
Sorting a Swift array by ordering from another array
(10 answers)
Closed 3 years ago.
In my json result, there is odds array. And inside of it, there will be 8 types of bet. And i want to sort the array by bet type as i want to show. Eg. first "name": "3Way Result", second another one, third "over/under" etc..
Here is my json result from server.
{
"success": true,
"result": [
{
"league_id": 5,
"localTeam": {"data": {}},
"visitorTeam": {"data": {}},
"scores": {},
"time": {"starting_at": {},},
"league": {"data": {"coverage": {}}},
"odds": [
{
"id": 12,
"name": "Over/Under",
"suspended": false,
"bookmaker": {
"data": [
{
"id": 2,
"name": "bet365",
"odds": {
"data": [
{
"label": "Over",
"value": "2.00",
"extra": null,
"probability": "50%",
"dp3": "2.000",
"american": 100,
"handicap": null,
"total": "2.5",
"winning": null,
"stop": false,
"bookmaker_event_id": 84922729,
},
{
"label": "Under",
"value": "1.80",
"probability": "55.56%",
"dp3": "1.800",
}
]
}
}
]
}
},
{
"id": 1,
"name": "3Way Result",
"suspended": false,
"bookmaker": {
"data": [
{
"id": 2,
"name": "bet365",
"odds": {
"data": [
{
"label": "1",
"value": "2.10",
"extra": null,
"probability": "47.62%",
"dp3": "2.100",
"american": 110,
},
{
"label": "X",
"value": "3.30",
"extra": null,
"probability": "30.3%",
"dp3": "3.300",
"american": 229,
},
{
"label": "2",
"value": "3.60",
}
]
}
}
]
}
},
{
"id": 975909,
"name": "Correct Score",
"suspended": false,
"bookmaker": {
"data": [
{
"id": 2,
"name": "bet365",
"odds": {
"data": [
{
"label": "1:0",
"value": "7.50",
"extra": null,
"probability": "13.33%",
"dp3": "7.500",
"american": 650,
"factional": null,
"handicap": null,
"total": null,
"winning": null,
"stop": false,
"bookmaker_event_id": 84922729,
"last_update": {
"date": "2020-02-20 10:59:06.746514",
"timezone_type": 3,
"timezone": "UTC"
}
},
{
"label": "2:0",
"value": "10.00",
},
]
}
}
]
}
},
],
"tipsters": 2
}
]
}
so it is not alphabetically, datetime or anytype i could access. How could i do ?
Update: I have added model.
struct BetterMatchResults: APIModel, Codable {
var success: Bool?
var result: [BetterMatch]?
}
struct BetterMatch: APIModel, Codable {
var id,_id: String?
var localTeam, visitorTeam: BetterTeam?
var spId, league_id, seasonID: Int?
var winningOddsCalculated: Bool?
var time: BetterTime?
var league: BetterLeague?
var createdAt, updatedAt: String?
var odds: [BetterOdd]!
var resultID: String?
var tipsters: Int?
var stats_url: String?
}
struct BetterLeague : APIModel, Codable {
var data: LeagueData?
}
struct LeagueData : APIModel, Codable{
var id: Int?
var active: Bool?
//var legacyID, countryID: Int?
var logo_path: String?
var name: String?
//var isCup: Bool?
}
struct BetterOdd : APIModel, Codable {
var id: Int?
var name: String?
var suspended: Bool?
var bookmaker: BetterBookmaker?
}
// MARK: - Bookmaker
struct BetterBookmaker : APIModel, Codable {
var data: [BetterBookmakerDatum]?
}
// MARK: - BookmakerDatum
struct BetterBookmakerDatum : APIModel, Codable {
var id: Int?
var name: String?
var odds: BetterOdds?
}
// MARK: - Odds
struct BetterOdds : APIModel, Codable {
var data: [BetterOddsDatum]?
}
class BetterOddsDatum: APIModel , Codable {
var label: String?
//var extra: NSNull?
//var probability, dp3: String?
var american: Int?
//var factional, handicap: NSNull?
var total: String?
var winning: Bool?
var stop: Bool?
var bookmakerEventID: Int?
//private var odd: Double
public var value: String?
init() {
}
}
If I understand your question correctly you want to be able sort the data based on betting type but the value of betting types are not sortable if used as a String variable. The solution would be to converting them into enum types with raw values and then sorting the array based on those raw values. Here is an example:
// Create a BetType for your datas
enum BetType: Int {
case overUnder = 0
case threeWayResult = 1 // 3WayResult
...
}
// Update your BetterOdd
struct BetterOdd : APIModel, Codable {
var id: Int?
var name: String?
var betType: BetType = .overUnder // or put your default value here
var suspended: Bool?
var bookmaker: BetterBookmaker?
}
// Loop your BetterMatch property's odds value after fetching datas.
for i in 0..<betterMatch.odds {
if betterMatch.odds[i].name == "over/under" {
betterMatch.odds[i].betType = .overUnder
}
... // Do the same for other types as well in else if blocks
}
Another alternative would be to add a function for getting the type in BetterOdd
struct BetterOdd ... {
... // Your properties
func getBetType() -> BetType {
if name == "over/under" {
return .overUnder
} else if name == "3WayResult" {
return .threeWayResult
}
... // Other cases
}
}
Finnaly for sorting you can do:
let result = betterMatch.odds.sorted({ $0.betType.rawValue > $1.betType.rawValue })
// or if you used the function solution
let result = betterMatch.odds.sorted({ $0.getBetType().rawValue > $1.getBetType().rawValue })
Since you are using a Codable approach you might need to loop the array and set the betType values based on name values.
i changed model
struct BetterOdd : APIModel, Codable {
var id: Int?
var name: String?
var suspended: Bool?
var bookmaker: BetterBookmaker?
//var betType: BetType = .threeWayResult
enum BetType:Int, APIModel, Codable {
case threeWayResult = 7
case overUnder = 6
case doubleChance = 5
case bothTeamsToScore = 4
case threeWayResultFirstHalf = 3
case threeWayResultSecondHalf = 2
case correctScore = 1
case hTfTdouble = 0
}
//
func getBetType() -> BetType {
if name == "3Way Result" {
return .threeWayResult
} else if name == "Over/Under" {
return .overUnder
} else if name == "Double Chance" {
return .doubleChance
} else if name == "Both Teams To Score" {
return .bothTeamsToScore
} else if name == "3Way Result 1st Half" {
return .threeWayResultFirstHalf
} else if name == "3Way Result 2nd Half"{
return .threeWayResultSecondHalf
} else if name == "Correct Score"{
return .correctScore
} else if name == "HF/FT Double" {
return .hTfTdouble
} else {
return .correctScore
}
}
}
and then :
let matchOddsArray = match.odds!
let result = matchOddsArray.sorted(by: { $0.betType.rawValue > $1.betType.rawValue})
let bet = result[indexPath.row]
works perfect.
First of all, mark result in BetterMatchResults and odds in BetterMatch as var, i.e.
struct BetterMatchResults: Codable {
let success: Bool
var result: [BetterMatch] //here....
}
struct BetterMatch: Codable {
var odds: [BetterOdd] //here....
//rest of the parameters...
}
Next, while parsing your JSON data, use map(_:) and sorted(by:) to modify the result and odds in the betterMatchResults, i.e.
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
var betterMatchResults = try JSONDecoder().decode(BetterMatchResults.self, from: data)
betterMatchResults.result = betterMatchResults.result.map {(betterMatch) in
var match = betterMatch
match.odds = match.odds.sorted(by: { $0.name < $1.name })
return match
}
//Test the response...
print(betterMatchResults.result.map({ $0.odds.map({ $0.name }) })) //[["3Way Result", "Correct Score", "Over/Under"]]
} catch {
print(error)
}

Finding a Struct in nested struct

I have been trying to find a struct inside the list of nested structs. Can anyone help me on this?
struct Places: PlacesProtocol {
private(set) var id: String
private(set) var name: String
private(set) var childPlaces: [PlacesProtocol]?
init(json: JSON) {
self.name = json[“Name”]
self.id = json[“Id”]
self. childPlaces = json[“ChildPlaces”].arrayValue.map { Places(json: $0) }
}
JSON:
{
"Id": "1",
"Name": "Place 1",
"ChildPlaces": [{
"Id": "12",
"Name": "Place 2",
"ChildPlaces": [{
"Id": "123",
"Name": "Place 3",
"ChildPlaces": [{
"Id": "1234",
"Name": "Place 4",
"ChildPlaces": null
}]
}, {
"Id": "13",
"Name": "Place 5",
"ChildPlaces": null
}]
}]
}
I have tried this:
nestedStruct.filter { $0.id == "13" }
I am able to parse this JSON in to the nested structure and I am trying to find a struct with Id. I have tried filter but it just filters only the first layer of the nested struct. Is there a way I can build recursive filter to find the struct which is deep inside the nested struct.
You could use a recursive function to perform a depth first search. Here's a rough example:
extension Place {
depthFirstSearch(where closure: (Place) -> Bool) -> Place? {
if closure(self) { return self }
else {
return self.chlidPlaces.first(where: {
$0.depthFirstSearch(where: closure)
})
}
}
}
let placeID13 = mainPlace.depthFirstSearch(where: { $0.id == "13" })

Swift - Cast from [SKNode] to unrelated type 'String' always fails

I am beginner in swift language and currently i am developing some App (Game).
But i have problem when casting AnyObject to String or else, which is always give warning "Cast from [SKNode] to unrelated type 'String' always fails"
Here my code
var facets = [AnyObject]()
init () {
facets = [
[
"id": "1",
"lang": ["id": "Memori", "en": "Memory"]
],
[
"id": "2",
"lang": ["id": "Kecepatan Berpikir", "en": "Speed"]
],
[
"id": "3",
"lang": ["id": "Fungsi Eksekutif", "en": "Control"]
],
[
"id": "4",
"lang": ["id": "Konsentrasi", "en": "Attention"]
],
[
"id": "5",
"lang": ["id": "Pemecahan Masalah", "en": "Problem Solving"]
]
]
}
func findFacetUsingId(id: String?) -> String? {
if let id = id {
for value in facets {
var facet_id: String = value["id"] as! String
if id == facet_id {
var names: Dictionary<String, String> = value["lang"] as! Dictionary<String, String>
return names[Lang.ID]
}
}
}
return nil
}
Here the screenshot,
By the way, I got success when using this code
var facet_id: String = value.objectForKey("id") as! String
instead of
var facet_id: String = value["id"] as! String
But the App to be slow (very very slow)
Thank you in advance
Actually your code works if you just do a little change:
func findFacetUsingId(id: String?) -> String? {
if let id = id {
for value in facets {
var facet_id: String = value["id"] as! String
if id == facet_id {
var names: Dictionary<String, String> = value["lang"] as! Dictionary<String, String>
return names["id"]
}
}
}
return nil
}