utahiosmac / Marshal

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

Handling JSON where top level structure is an array not a dictionary #106

Closed tomasebrennan closed 7 years ago

tomasebrennan commented 7 years ago

Hello,

Very interested in using Marshal however I'm not sure how best to handle JSON where the top level structure is an array rather than a dictionary.

We have some JSON that looks like this:

 [
  {
  "id": "111",
  "firstname":"John",
  "surname": "Doe"
  }, {
  "id": "222",
  "firstname":"Jane",
  "surname": "Doe"
}
]

We would like to be able to map this directly into say an array of Users where User conforms to Unmarshaling i.e.

struct User: Unmarshaling {
    var id: Int
    var firstname: String
    var surname: String

    init(object: MarshaledObject) throws {
        id = try object.value(for: "id")
        firstname = try object.value(for: "firstname")
        surname = try object.value(for: "surname")
    }
}

However the library seems to always need a key in order to create the unmarshaled object. The only way I can see to do this now is to have something like:

var jsonObjects: [JSONObject] = try! JSONParser.JSONArrayWithData(data)
    for obj in jsonObjects {
         let user = User()
         user.id = try obj.value("id")
         user.firstname = try obj.value("firstname")
        user.surname = try obj.value("surname")
    }

This does not seem as elegant as being able to create the array of users directly i.e.

let users: [User] = try json.value("users")

What is the best way of handling this kind of JSON format using Marshal?

Thanks

stevenkramer commented 7 years ago

@tomasebrennan not sure if this will work, but I had a similar problem. In my case I found it was simply not (very clearly) documented, but well-supported. Construct top-level objects right from their dictionary/array representations using init(object:) or value(from:)

let json = (try? JSONSerialization.jsonObject(with: document, options: JSONSerialization.ReadingOptions()))  as? NSDictionary,
let topLevelObject = try? ObjectResponse(object: json)
....
let topLevelArray = try? [ArrayElement].value(from: json, discardingErrors: false)
jarsen commented 7 years ago

This case is a little awkward: Marshal really is designed for dealing with JSON objects, and not JSON Arrays.

One way to deal with this is to put your array inside and object, and then extract it.

let object = { "users": jsonObjects }
let users: [User] = try object.value(for: "users")

You can also use the handy discardingErrors: flag as @stevenkramer mentions, which means you will end up with an array of values without the invalid items, rather than the entire expression throwing an error when one of the array's elements doesn't match the defined schema.

bwhiteley commented 7 years ago

I usually map it with one or two lines. An example can be seen in #85.

tomasebrennan commented 7 years ago

Thanks I'll stick with @jarsen solution for now.

jarsen commented 7 years ago

I'm going to go ahead and close this. Be sure to let us know if you have more questions.