简体   繁体   中英

Decoding JSON for single object vs array of objects in swift

I'm fairly new to the swift programming language and have been trying to get this to work for the last week or so. I'm working with an existing API that returns JSON data whose structure changes a little depending on how many venues get returned.

The real structure is somewhat more complicated, but this example illustrates the problem. In one flavor of result, I get a single venue returned like:

{
  "totalItems": 21,
  "pageSize": 2,
  "venues": {
    "venue":
    {
       "name": "Venue Name 1"
       "location": "Location A",
       "showCount": "4"
    }
  }
}

In another flavor of result, I get an array of venues returned:

{
  "totalItems": 21,
  "pageSize": 2,
  "venues": {
    "venue":
    [{
       "name": "Venue Name 1"
       "location": "Location A",
       "showCount": "4"
    },
    {
       "name": "Venue Name 2"
       "location": "Location B",
       "showCount": "2"
    }]
  }
}

Yes - the owner of this API should have returned an array regardless, but they didn't and it cannot be changed.

I was able to get this to decode properly for an array of venues (or even if no venues were passed), but it aborts when a single venue is passed (due to the structure variation of course). My code also worked when I changed it to accommodate a single venue, but then returns of multiple venues aborted.

What I'd like to do is decode either variation into an internal structure containing an array regardless of which variation I receive, thus making things far simpler for me to deal with programmatically afterward. Something like this:

struct Response: Decodable {
    let totalItems: Int
    let pageSize: Int
    let venues: VenueWrapper

    struct VenueWrapper: Decodable {
        let venue: [Venue]     // This might contain 0, 1, or more than one venues
    }

    struct Venue: Decodable {
        let name: String
        let location: String
        let showCount: Int
    }
}

Note: In the actual JSON response, there are actually several substructures like this in the response (eg, a single structure vs an array of structures) which is why I felt simply creating an alternate structure was not a good solution.

I'm hoping someone has come across this before. Thanks in advance!

You can create your own decoder,

struct Response: Decodable {
    let totalItems: Int
    let pageSize: Int
    let venues: VenueWrapper

    struct VenueWrapper: Decodable {
        var venue: [Venue]

        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            venue = []
            if let singleVenue = try? values.decode(Venue.self, forKey: CodingKeys.venue) {
                //if a single venue decoded append it to array
                venue.append(singleVenue)
            } else if let multiVenue = try? values.decode([Venue].self, forKey: CodingKeys.venue) {
                //if a multi venue decoded, set it as venue
                venue = multiVenue
            }

            enum CodingKeys: String, CodingKey { case venue }
        }
    }

    struct Venue: Decodable {
        let name: String
        let location: String
        let showCount: String
    }
}

There's no need for VenueWrapper . 🙅🎁

struct Response {
  let totalItems: Int
  let pageSize: Int
  let venues: [Venue]

  struct Venue {
    let name: String
    let location: String
    let showCount: Int
  }
}

You'll need to write your own initializer. Unfortunately, I don't think there's a way to eliminate the boilerplate for the not-goofily-encoded properties.

Even if you never need to encode it, making Response Codable , not just Decodable , will give you access to an auto-generated CodingKeys .

extension Response: Codable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    totalItems = try container.decode(Int.self, forKey: .totalItems)
    pageSize = try container.decode(Int.self, forKey: .pageSize)
    venues = try container.decode(key: .venues)
  }
}

That last line relies on a protocol and extension which you can use for any other types that are similarly "encoded". 🙃

protocol GoofilyEncoded: Codable {
  /// Must have exactly one case.
  associatedtype GoofyCodingKey: CodingKey
}

extension KeyedDecodingContainer {
  func decode<Decodable: GoofilyEncoded>(key: Key) throws -> [Decodable] {
    let nestedContainer = try self.nestedContainer(
      keyedBy: Decodable.GoofyCodingKey.self,
      forKey: key
    )

    let key = nestedContainer.allKeys.first!

    do {
      return try nestedContainer.decode([Decodable].self, forKey: key)
    }
    catch {
      return try [nestedContainer.decode(Decodable.self, forKey: key)]
    }
  }
}

All your types that might be encoded in arrays, or not 🤦‍♂️, will need a one-case enum, like so:

extension Response.Venue: GoofilyEncoded {
  enum GoofyCodingKey: CodingKey {
    case venue
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM