Search code examples
arraysswiftjson-deserialization

How to represent nested array types for EVReflection


I'm trying to represent a JSON structure in swift for use with EVReflection. The string looks like this:

{
    "date": 2457389.3333330001, 
    "results": 
        {
            "sun": [[559285.95145709824, 202871.33591198301, 61656.198554897906], [127.6163120820332, 948.44727756795123, 406.68471093096883]], 
            ... etc ...
            "geomoon": [[-401458.60657087743, -43744.769596474769, -11058.709613333322], [8433.3114508170656, -78837.790870237863, -26279.67592282737]]
        }, 
    "unit": "km"
}

I've tried several approaches to model the inner "results" dictionary, which is keyed with a string and always has values that are an array of two elements, each of which have three doubles (i.e., [String: [[Double]]] is one model I've tried). No matter what I do, I get an error along these lines:

ERROR: valueForAny unkown type , type _NativeDictionaryStorage<String, Array<Array<Double>>>. Could not happen unless there will be a new type in Swift.

Does anyone have an idea how to model this so EVReflection is happy? I've also had the sense from the phrase Could not happen unless there will be a new type in Swift that there's another method or different modeling with explicit subtypes I can add that would give it what it needs, but I can't puzzle out what to do from the docs.


Solution

  • In Swift a Double is a struct. Doing reflections with structs have some limitations. I prefer using NSNumber instead.

    My first thought would be an object model like this:

    class MyObject: EVObject {
        var date: NSNumber?
        var results: Result?
        var unit: String?
    }
    
    class Result: EVObject {
        var sun: [[NSNumber]]?
        var geomoon: [[NSNumber]]?
    }
    

    But your object has a nested value. Until now I never have tested EVReflection with a structure like this. I will try adding a unit test for this in a moment. It would have been easier if the inner array was an object (something like an object with an x, y and z value?)

    Update: This seems to work. I tested this with the following unitTest:

    func testNestedArry() {
        let json = "{\"date\": 2457389.3333330001,\"results\": { \"sun\": [[559285.95145709824, 202871.33591198301, 61656.198554897906], [127.6163120820332, 948.44727756795123, 406.68471093096883]], \"geomoon\": [[-401458.60657087743, -43744.769596474769, -11058.709613333322], [8433.3114508170656, -78837.790870237863, -26279.67592282737]] }, \"unit\": \"km\" }"
        let myObject = MyObject(json: json)
        print(myObject.toJsonString())
    }
    

    And the output of myObject was:

    {
      "date" : 2457389.333333,
      "results" : {
        "sun" : [
          [
            559285.9514570982,
            202871.335911983,
            61656.19855489791
          ],
          [
            127.6163120820332,
            948.4472775679512,
            406.6847109309688
          ]
     ],
        "geomoon" : [
          [
            -401458.6065708774,
            -43744.76959647477,
            -11058.70961333332
          ],
          [
            8433.311450817066,
            -78837.79087023786,
            -26279.67592282737
          ]
        ]
      },
      "unit" : "km"
    }
    

    Update 2: As discussed below the planet 'keys' are not a fixed set. So actually this should be handled as a dictionary. To do something like this you can use a 'hack' by using the setValue forKey to populate a dictionary. For doing this you would need a data model like this:

    class MyObject: EVObject {
        var date: NSNumber?
        var results: Result?
        var unit: String?
    }
    
    class Result: EVObject {
        var planets = [String: [[NSNumber]]]()
    
        // This way we can solve that the JSON has arbitrary keys
        internal override func setValue(value: AnyObject!, forUndefinedKey key: String) {
            if let a = value as? [[NSNumber]] {
                planets[key] = a
                return
            }
            NSLog("---> setValue for key '\(key)' should be handled.")
        }
    }
    

    You do have to be aware that if you want to create json for this, that you will have an extra level in your hierarchy for planets. If you don't want this, then there is a workaround possible by changing the results property to String: [[NSNumber]] You would then also need to implement the EVCustomDictionaryConverter. See the workaround unit tests to see how that would work.