utahiosmac / Marshal

Marshaling the typeless wild west of [String: Any]
MIT License
697 stars 62 forks source link

How to marshal array of arrays? #76

Closed jlyonsmith closed 7 years ago

jlyonsmith commented 7 years ago

I'm trying to marshal a property like:

var arrayOfArrayOfThings: [[Thing]]? 
// ... later on ...
arrayOfArray = try object.value(for: "arrayOfArrayOfThings") 

Where Thing is a Unmarshaling. But I get a No 'value' candidates produce the expected contextual result type '[[Thing]]?'. I tried defining extensions to MarshableObject.value that return [[A]] but got stuck. It seems like for Array<Array<A>> Swift doesn't recognize the inner Array<A> type as a ValueType?

jlyonsmith commented 7 years ago

Here's a more complete example of the problem. Paste this into a playground with the Marshal sources:

public struct Tableau: Marshaling, Unmarshaling {
    public var matrix: [[Int]]?

    public init(object: MarshaledObject) throws {
        self.matrix = try object.value(for: "matrix")
    }

    public init(matrix: [[Int]]?) {
        self.matrix = matrix
    }

    public func marshaled() -> [String: Any] {
        return [
            "matrix": matrix ?? [],
        ]
    }
}
jarsen commented 7 years ago

Sorry. I read this the other day and then got pulled away while trying to figure out a good response and then the weekend happened.

This is a really good question: I'm not sure what the answer is here. It's not a case I've dealt with yet.

We have support for value(for:) -> [A] on MarshaledObject, and [ValueType] conforms to Marshal.ValueType. So in theory, I would expect an array of arrays of ValueType to work, but apparently it doesn't. I'm going to file this as a bug.

We may need to add explicit support for [[A]] on MarshaledObject.

brianmullen commented 7 years ago

@jarsen An array doesn't conform to ValueType which is why an array of arrays don't work. I think we could add support for it by extending MarshaledObject with a version of value(for:) -> [A] where A doesn't conform to ValueType and it would call a new value(from:) method on Array where Element doesn't conform to ValueType.

jarsen commented 7 years ago

AH. Yes you're right. I misread the extension Array where Element: ValueType 😝

That could work.

alexsmith84 commented 7 years ago

Hi, I'm trying to deserialize like so:

let object = try! JSONParser.JSONObjectWithData(jsonData)
let array: [[String: Any]] = try object.value(for: "data.arrayOfDictionaries")

I'd really love to be able to do this! I feel like this basically the same issue as described above.

KingOfBrian commented 7 years ago

I was looking into this to see how #86 impacted this, it now won't compile instead of not working.

let names: [[String]] = try! json.value(for: "names")

I think this is better behavior, and should just need more methods to support all of the possibilities. I want to check in here before I add all of these possibilities though. It will almost double the amount of methods in here to fully support nested arrays, which is a bit painful. Array and Dictionary are ballooning the number of value(for:) functions a lot. I believe this will clean up nicely when Swift 4 lands with constrained protocol conformances. It may make more sense to not support nested arrays until Swift gets constrained protocol conformances.

jarsen commented 7 years ago

I'm of the mind to not add explicit support, and leave that as an exercise to those who need it. I'd rather keep things simple, especially if they're more edge case.

jarsen commented 7 years ago

Given our discussion up to this point, I'm going to suggest that Marshal itself will not add the functionality necessary for arrays of arrays. I am willing to revisit the topic if someone comes with a compelling proposal/PR. Thanks!

jarsen commented 7 years ago

@Shmaff I believe with some of the latest changes that unmarshaling [MarshaledObject] should work. I think it is a different issue though from the nested arrays. If you would like to verify, that would be great! If not feel free to open up a new issue :)

amarcadet commented 7 years ago

I'm facing almost the same issue of having to parse an array of arrays of arrays \o/.

"boundaries": [
  [
    [
      0.148271,
      51.6723432
    ],
    [
      0.148271,
      51.3849401
    ],
    [
      -0.3514683,
      51.3849401
    ],
    [
      -0.3514683,
      51.6723432
    ],
    [
      0.148271,
      51.6723432
    ]
  ]
]

Since the last nested array level is coordinates, I've figured out that I could make CLLocationCoordinate2D conforms to ValueType.

extension CLLocationCoordinate2D: ValueType {

    public static func value(from object: Any) throws -> CLLocationCoordinate2D {
        guard let array = object as? [Double] else {
            throw MarshalError.typeMismatch(expected: [Double].self, actual: type(of: object))
        }

        guard array.count == 2 else {
            throw MarshalError.typeMismatch(expected: "Array expected to contains 2 values", actual: "Array contains \(array.count) value(s)")
        }

        return CLLocationCoordinate2D(latitude: array[0], longitude: array[1])
    }

}

However I couldn't figure out how to parse the array of arrays. This trigger a compile error as expected by reading this comment:

let coordinates: [[CLLocationCoordinate2D]] = try object.value(for: "boundaries") 

I'm considering switching to Marshal but this is a key feature for me.

Of course I can't change the JSON format, that would be too easy...

gereons commented 7 years ago

+1 for adding multi-dimensional array support to Marshal.

@amarcadet: I currently need to parse similar data (simple polygons from a GEOJson-compatible format), and use this:

let coords = try object.any(for: "coordinates")
if let coordinates = coords as? [[[Double]]] {
    self.coordinates = coordinates[0].map { CLLocationCoordinate2D(latitude: $0[0], longitude: $0[1]) }     } 
else {
    throw MarshalError.typeMismatchWithKey(key: "coordinates", expected: "[[[Double]]]", actual: coords)
}
jslew commented 7 years ago

Do the Swift 3.1 generic constraint additions make it any more possible to support this? I had a quick look, but couldn't make it work.

yanbraslavsky-home24 commented 7 years ago

So is there any ugly workaround at least ? I am using Marshal and I love it , but how should I parse 2 dimensional arrays ?

yurii-voievodin commented 6 years ago

I do it like this:

let anyObject: Any = try object.any(for: "json_key")
let matrix = handle(anyObject)
func handle(_ object: Any) -> [[CustomStruct]] {
    var map: [[CustomStruct]] = []

    if let array = object as? [Any] {
      for subArrayItem in array {
        if let subArray = subArrayItem as? [Any] {

          var row: [CustomStruct] = []
          for item in subArray {
            if let json = item as? [String: Any] {

              if let brick = try? CustomStruct(object: json) {
                row.append(brick)
              }
            }
          } 
          map.append(row)
        }
      }
    }
    return map
  }