evermeer / AlamofireJsonToObjects

An Alamofire extension which converts JSON response data into swift objects using EVReflection
Other
161 stars 28 forks source link

Array mapping return empty array #39

Closed aliasdoc closed 7 years ago

aliasdoc commented 7 years ago

Hi,

I try to map an array of objects like this:

Models

open class ApiGenericBase: EVNetworkingObject {
    var success: NSNumber?
    var message: String?
    var status_code: String?
}

open class ApiResponse<T>: ApiGenericBase, EVGenericsKVC where T:NSObject {
    var data: T = T()

    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "data":
            data = value as? T ?? T()
        default:
            print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
        }
    }

    public func getGenericType() -> NSObject {
        return T()
    }
}

public class Vehicle: EVNetworkingObject {
    var uuid: String?
    ...
}

open class List<T>: EVNetworkingObject, EVGenericsKVC where T:NSObject {
    var records: [T] = [T]()

    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        records = value as? [T] ?? [T]()
    }

    public func getGenericType() -> NSObject {
        return [T]() as NSObject
    }

}

and I'm calling like this:

manager.request(GetVehicleFromArgusPlateProvider, method: .get,
                        parameters: parameters,
                        encoding: URLEncoding.default,
                        headers: nil).responseObject { (response: DataResponse<ApiResponse<List<Vehicle>>>) in
            ...
        }

but when I dump response I have an empty array:

dump(response)
▿ _TtGC9xxx_v24ListCS_7Vehicle_ = {
  "records" : [

  ]
} #0
  - super: AlamofireJsonToObjects.EVNetworkingObject
    ▿ super: EVReflection.EVObject
      - super: NSObject
      - evReflectionStatuses: 0 elements
  - records: 0 elements

My JSON server response:

{
  "data": [
    {
      "uuid": "52ec7e70-eb02-11e6-b6f3-1bb7c380e65e",
      ...
    },
    {
      "uuid": "53230b60-eb02-11e6-aee4-47e7e9e49d51",
      ...
    },
    {
      "uuid": "5366e810-eb02-11e6-93b6-91512741054a",
      ...
    },
    {
      "uuid": "53aa9cd0-eb02-11e6-a958-215ccabe8e51",
      ...
    }
  ]
}

Any idea or help please. Thanks.

evermeer commented 7 years ago

The first thing i see is that it should be:

public func getGenericType() -> NSObject {
        return T() 
    }

You can see a unit test for generic arrays in EVReflectionWorkaroundSwiftGenericsTests.swift

aliasdoc commented 7 years ago

Hi @evermeer , thank you, I change return [T]() as NSObjectby return T()but always return nil. I think it's because I don't have a key for my Listmodel, when I try to print keys, it prints keys of all Vehicle models in array like this:

open class List<T: EVNetworkingObject>: EVObject, EVGenericsKVC {
    var records: [T]?

    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        print("key: \(key) => value: \(value)")
        records = value as? [T]
    }

    public func getGenericType() -> NSObject {
        return T() 
    }

}

key: uuid => value: Optional(52ec7e70-eb02-11e6-b6f3-1bb7c380e65e)  <- keys of Vehicle model 1
...
key: uuid => value: Optional(53230b60-eb02-11e6-aee4-47e7e9e49d51) <- keys of Vehicle model 2
...
key: uuid => value: Optional(5366e810-eb02-11e6-93b6-91512741054a) <- keys of Vehicle model 3
...
``

I see unit test EVReflectionWorkaroundSwiftGenericsTests.swift for generic and try a lot of things but not found any solution.

Thank you.
evermeer commented 7 years ago

Ah, I see some more inconsistencies. In your json the data key is an array and in your model you defined it as: var data: T = T() This should be var data: [T] = [T]() Of course you should then also change the setGenericValue assignment for this It looks like you don't need your List class at all.

Let me try to get up with the correct implementation:

open class ApiGenericBase: EVNetworkingObject {
    var success: NSNumber?
    var message: String?
    var status_code: String?
}

open class ApiResponse<T>: ApiGenericBase, EVGenericsKVC where T:NSObject {
    var data: [T] = [T]()

    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "data":
            data = value as? [T] ?? [T]()
        default:
            print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
        }
    }

    public func getGenericType() -> NSObject {
        return T()
    }
}

public class Vehicle: EVNetworkingObject {
    var uuid: String?
    ...
}    
}

And then you call it like this:

manager.request(GetVehicleFromArgusPlateProvider, method: .get,
                        parameters: parameters,
                        encoding: URLEncoding.default,
                        headers: nil).responseObject { (response: DataResponse<ApiResponse<Vehicle>>) in
            ...
        }

I did not test the code above. If you still have a problem with it, then let me know. I will then create a working unit test for you.

aliasdoc commented 7 years ago

Thank you @evermeer but data attribute of ApiResponse model can be an array OR an item, it's not possible to do this ?

evermeer commented 7 years ago

Ah, that's a little more difficult.

I think that originally I had set this up that if only 1 item was returned and the object was an array, that it was put in an array with that one element. I have to do some tests to see if that is stil the case.

I think it still needs to be handled in the setGenericValue (see code below) I will do some tests this evening to see how this works. (in about 6 hours)

public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "data":
            if let d = value as? [T] {
                data = d
            } else if let d = value as? T() {
                data = [T]()
                data.append(d)
            } else {
               data = [T]() 
           }
        default:
            print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
        }
    }
aliasdoc commented 7 years ago

Thank you for your help @evermeer , I just try your code but Xcode with some modifications (Xcode errors):

'[T]' is not convertible to 'T'
open class ApiResponse<T>: ApiGenericBase, EVGenericsKVC where T:NSObject {
    var data: [T] = [T]()

//    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
//        switch key {
//        case "data":
//            data = value as? T ?? T()
//        default:
//            print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
//        }
//    }

    public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "data":
            if let d = value as? [T] {
                data = d
            } else {
                if let d = value as? T {
                    data = [T]()
                    data.append(d)
                } else {
                    data = [T]()
                }
            }
        default:
            print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
        }
    }

    public func getGenericType() -> NSObject {
        return T()
    }
}

It breaks all my code for another api endpoints, a good solution is to create a generic array (my List model) who inherits from Array, no ?

evermeer commented 7 years ago

I added a unit test that shows you how this works with an array or a single object. You can find it here: https://github.com/evermeer/EVReflection/blob/master/UnitTests/EVReflectionTests/EVReflectionIssueAF39.swift I noticed I did not need the structure from my last response above. It just works by setting the data. As you can see at the testGenericsJson3 function you can you whatever object you wish for the generic object.

I know It's not using Alamofire for getting the json but the object structure would be the same. It's easier for me to do only this, otherwise i would need 2 valid responses from the internet for the 2 variants.

evermeer commented 7 years ago

One other thing... You should use: pod 'EVReflection/Alamofire' Instead of: pod 'AlamofireJsonToObjects' Functionally the are now still the same, but future support for Alamofire will only be done on the EVReflection library.

aliasdoc commented 7 years ago

Thank you for your help @evermeer, I test it tomorrow.

aliasdoc commented 7 years ago

Hi @evermeer, your solution is working but cannot parse empty array represented like this:

{
  "data": {
    "uuid": "d6c132e0-eeea-11e6-a890-09eff8d5a7a3",
    ...
    "vehicles": {} <- AlamofireJsonToObjects  create an empty field for this representation
  }
}
public class User: EVNetworkingObject {
    var uuid: String?
    ...
    var vehicles:[UserVehicle]?
}

public class UserVehicle: EVNetworkingObject {
    var uuid: String?
    ...
}

manager.request(...).validate().responseObject { (response: DataResponse<ApiResponse<User>>) in

        }

dump of response:

▿ Optional(User = {
  ...
  "vehicles" : [
    {
      "vehicle" : null,
      "uuid" : null,
      "plate_identifier" : null,
      "color" : null
    }
  ],
  ...
})
  ▿ some: User = {
  ...
  "vehicles" : [
    {
      "vehicle" : null,
      "uuid" : null,
      "plate_identifier" : null,
      "color" : null
    }
  ],
  ...
} #0
    - super: AlamofireJsonToObjects.EVNetworkingObject
      ▿ super: EVReflection.EVObject
        - super: NSObject
        - evReflectionStatuses: 0 elements
    ...
    ▿ vehicles: Optional([UserVehicle = {
  "vehicle" : null,
  "uuid" : null,
  "plate_identifier" : null,
  "color" : null
}])
      ▿ some: 1 element
        ▿ UserVehicle = {
  "uuid" : null,
  ...
} #1
          - super: AlamofireJsonToObjects.EVNetworkingObject
            ▿ super: EVReflection.EVObject
              - super: NSObject
              - evReflectionStatuses: 0 elements
          - uuid: nil
          - plate_identifier: nil
          - vehicle: nil
          - color: nil
evermeer commented 7 years ago

Hmm... not sure what to do with this one. Since you specified this: "vehicles": { } Which is different from: "vehicles": [] or this: "vehicles": nil In your case you were actually saying that you have an object in your vehicles property. { } means there is an object. If you wanted an empty array, then it should say [] So I think you need a workaround for solving this issue in your case.

I think you could add this to your Vehicle object:

   override public func initValidation(dict: NSDictionary) {
      if dict.count == 0 {
         self.addStatusMessage(.Custom, message: "Empty object is not allowed")
      }
   }

Then the object will still be in the array, but you could easily filter it out.

evermeer commented 7 years ago

If you need any more help, then just let me know.

aliasdoc commented 7 years ago

Hi @evermeer, sorry for my late answer, all is good thank you I modify my server response.