I'm writing in Swift 3.1, using ObjectMapper to map my JSON response to my models.
I'm trying to map this rather complex JSON response with dynamic keys and am hoping to get some feedback on what I'm doing wrong.
A group has statistics about it's progress. It has stats broken down to years and then months. Each month within a year has results, ROI and win. The ROI and win are just percentages but the results key is fixed with the keys below, 1-5, and then some integer as a value.
My JSON
"stats": {
"2017": {
"1": {
"results": {
"1": 13,
"2": 3,
"3": 1,
"4": 1,
"5": 0
},
"roi": 0.40337966202464975,
"win": 0.8181818181818182
},
"2": {
"results": {
"1": 13,
"2": 5,
"3": 1,
"4": 2,
"5": 1
},
"roi": 0.26852551067922953,
"win": 0.717948717948718
}
}
}
My models
class GroupResponse: Mappable {
var stats: [String: [String: StatsMonthResponse]]?
func mapping(map: Map) {
stats <- map["stats"]
}
}
class StatsMonthResponse: Mappable {
var tips: [String: Int]?
var roi: Double?
var win: Double?
func mapping(map: Map) {
tips <- map["results"]
roi <- map["roi"]
win <- map["win"]
}
}
What I get
The response I get has the stats property in my GroupResponse class, as nil.
What other approach could I do to accomplish this, or change in my implementation to get this done?
Solution
I solved my problem by mapping the JSON manually.
class GroupResponse: Mappable {
var stats: [String: StatsYear]?
func mapping(map: Map) {
stats <- map["stats"]
}
}
class StatsYear: Mappable {
var months: [String: StatsMonth] = [:]
override func mapping(map: Map) {
for (monthKey, monthValue) in map.JSON as! [String: [String: Any]] {
let month = StatsMonth()
for (monthKeyType, valueKeyType) in monthValue {
if monthKeyType == "results" {
let tipResultDict = valueKeyType as! [String: Int]
for (result, tipsForResult) in tipResultDict {
month.tips[result] = tipsForResult
}
}
else if monthKeyType == "roi" {
month.roi = valueKeyType as? Double
}
else if monthKeyType == "win" {
month.win = valueKeyType as? Double
}
}
months[monthKey] = month
}
}
}
class StatsMonth {
var tips: [String: Int] = [:]
var roi: Double?
var win: Double?
}
There's probably a better solution to this problem but this is what I'm sticking with for now.
Hopefully this helps!
Related
I have a problem with my project, when my App starts, the configuration is automatically updated from the server, the json from server like this:
{
"version":1
"config":
{
"key1": {xxx}
"key2": {xxx}
"key3": {xxx}
"key4": {xxx}
"key5": {xxx}
"key6": {xxx}
}
And I use the ObjectMapper to convert json to model, like this:
struct GlobalConfig: Mappable {
var version = 0
var key1: [String: [LocalizedText]]?
var key2: [RouteObject]?
var key3: UrlConfig?
var key4: [String: [String: [[String: Any]]]]?
var key5: DegreeInfoList?
var key6: [String: String]?
init?(map: Map) { }
mutating func mapping(map: Map) {
version <- map["version"]
key1 <- map["key1"]
key2 <- map["key2"]
key3 <- map["key3"]
key4 <- map["key4"]
key5 <- map["key5"]
key6 <- map["key6"]
}
Now there is a problem, when the json from server is large, it will consume a lot of traffic. In fact, some configurations do not need to be updated. So the json from server will like this:
{
"version":2
"config":
{
"key1": {xxx}
"key2": {xxx}
}
It means key1 and key2 will be updated, key3, key4, key5 and key6 keep the old values.
How to handle the JSON?
I don't understand what's the Mapper object is. It can be done with Codable.
You can do like this to update your local config with incoming config.
struct GlobalConfig: Codable {
var version = 0
var key1: [String: [LocalizedText]]?
var key2: [RouteObject]?
var key3: UrlConfig?
var key4: [String: [String: [[String: Any]]]]?
var key5: DegreeInfoList?
var key6: [String: String]?
mutating func combine(with config: GlobalConfig) {
if let k = config.key1 { key1 = k }
if let k = config.key2 { key2 = k }
...
}
}
class App {
var config: GlobalConfig
func receiveConfig(newConfig: GlobalConfig) {
config.combine(with: newConfig)
}
}
Edited :
In response to comment, here a possible solution for 'deep' combine.
Very roughly :
extension Dictionary {
func combine(with dictionary: Dictionary) {
}
}
extension Array {
func combine(with array: Array) {
}
}
protocol Combinable: Codable {
func combine(with object: Self)
}
struct GlobalConfig: Combinable {
var version = 0
var key1: [String: [LocalizedText]]?
var key2: [RouteObject]?
mutating func combine(with config: GlobalConfig) {
if let k = config.key1 { key1.combine(with: k) }
if let k = config.key2 { key2.combine(with: k) }
...
}
}
This is another subject, but types like [String: [String: [[String: Any]]]] should be avoided.
It is hard to read, debug, and combine ;)
I think it is better to use structured sub-models. Much safer, scalable and 'documentable'.
struct GlobalConfig: Codable {
...
// Old format: var key4: [String: [String: [[String: Any]]]]?
var key4: MegaSettings?
...
}
struct MegaSettings: Combinable {
struct ScreenOptions: Combinable {
var options: [String: Any]
}
struct DisplayOptions: Combinable {
struct DeviceDisplayOptions {
var screen: [ScreenOptions]
}
var optionsPerDevice: [String: DeviceDisplayOptions]
}
var displayOptions: [String: DisplayOptions]
}
I'm trying to map JSON of an Array type to a Dictionary and i'm not quite sure how to do it using ObjectMapper.
Example JSON:
{
"colors": [
{
"id": "red",
"r": "255",
"g": "255",
"b": "255"
}
]
}
You could do the following. Map it to an array first then using the didSet map it to the dictionary.
class MyClass: Mappable {
private var arrayColors = [MyClass2] {
didSet {
var mapTypes = [String:MyClass2]?
for obj in arrayColors {
mapTypes[obj.id] = obj
}
types = mapTypes
}
}
var colors:[String:MyClass2] = [String:MyClass2]()
func mapping(map: Map) {
arrayColors <- map["colors"]
}
}
I have a Json type of an Array of dictionaries, but I can only store the first dictionary which has the type "rent":
"prices": [
{
"type": "rent",
"currency": "MY",
"min": 1000000,
"max": 5000000
},
{
"type": "buy",
"currency": "MY",
"min": 4000000,
"max": 5000000
}
],
So, in Listing.Swift, I call transform class to convert it into dictionary:
Listing.swift
_price <- (map["prices"], ListingPriceTransform())
ListingPriceTransform.swift
class ListingPriceTransform: TransformType {
typealias Object = ListingPrice
typealias JSON = [[String: AnyObject]]
func transformFromJSON(value: AnyObject?) -> Object? {
guard let jsonDictionariesArray = value as? [[String: AnyObject]] else { return nil }
for object in jsonDictionariesArray {
return ListingPrice.createModel(object)
}
return ListingPrice.createModel(Object)
}
func transformToJSON(value: Object?) -> JSON? {
return nil
}
}
ListingPrice.swift
func mapping(map: Map) {
type <- map["type"]
currency <- map["currency"]
minPrice <- (map["min"], IntTransform())
maxPrice <- (map["max"], IntTransform())
}
So, I am failing to save probably in ListingPriceTransform (for loop) to get dictionaries from array before passing into Map in ListingPrice.swift.
I'm currently using AlamofireObjectMapper to create JSON objects. I'm having a tough time accessing JSON arrays:
This is the JSON Array:
[
{
"city": "string",
"country": "string",
"county": "string",
}
]
This is the function:
func getMemberDetails ( id: String) {
Alamofire.request(.GET, "\(baseURL)/api/Members/\(id)/memberDetails").responseArray { (response: Response<[MemberDetailInfo], NSError>) in
let memberDetailArray = response.result.value
if let memberDetailArray = memberDetailArray {
for memberDetail in memberDetailArray {
print(memberDetail.createDate)
print(memberDetail.id)
}
}
}
}
This Is the class:
class MemberDetailInfo: Mappable{
var city: String?
var country: String?
var county: String?
required init?(_ map: Map) {
mapping(map)
}
unc mapping(map: Map) {
city <- map["city"]
country <- map["country"]
county <- map["county"]
}
}
Whenever I step through it it just jumps right to the end, I’m not sure why it isn’t working. If anyone knows how to extract the JSON data from the array it would be greatly appreciated.
Given the JSON data:
{
"location": "Toronto, Canada",
"three_day_forecast": [
{
"conditions": "Partly cloudy",
"day" : "Monday",
"temperature": 20
},
{
"conditions": "Showers",
"day" : "Tuesday",
"temperature": 22
},
{
"conditions": "Sunny",
"day" : "Wednesday",
"temperature": 28
}
]
}
And the following classes:
class WeatherResponse: Mappable {
var location: String?
var threeDayForecast: [Forecast]?
required init?(_ map: Map){
}
func mapping(map: Map) {
location <- map["location"]
threeDayForecast <- map["three_day_forecast"]
}
}
class Forecast: Mappable {
var day: String?
var temperature: Int?
var conditions: String?
required init?(_ map: Map){
}
func mapping(map: Map) {
day <- map["day"]
temperature <- map["temperature"]
conditions <- map["conditions"]
}
}
I have in my ViewController:
class ViewController: UIViewController {
var threeDayForecastArray = [Forecast]()
override func viewDidLoad() {
super.viewDidLoad()
getWeatherReport()
print(threeDayForecastArray.count) // 0
print(threeDayForecastArray[0].conditions) //fatal error: Index out of range
}
func getWeatherReport() {
let URL = "https://raw.githubusercontent.com/tristanhimmelman/AlamofireObjectMapper/d8bb95982be8a11a2308e779bb9a9707ebe42ede/sample_json"
Alamofire.request(.GET, URL).responseObject { (response: Response<WeatherResponse, NSError>) in
let weatherResponse = response.result.value
print(weatherResponse?.location)
if let threeDayForecast = weatherResponse?.threeDayForecast {
self.threeDayForecastArray = threeDayForecast
for forecast in threeDayForecast {
print(forecast.day)
print(forecast.temperature)
}
}
}
}
}
I'm trying to populate threeDayForecastArray, but it's giving me count = 0 or an out of index error. Any advice on what I'm doing wrong or help are welcome.