-1

I am working through an online tutorial that makes use of the Whitehouse Petitions JSON feed. I am using Swift 5 and have implemented my structs to conform to Codable. The Whitehouse JSON causes a Codable error. Below is a snippet of the JSON. The part that is causing trouble is "response". In the first example it is shown like I would expect an empty array to be, but when it has a value as in the second example, the JSON switches from [] to {} as shown below.

[ ] example:

  "results":[
      {
         "id":"2722358",
         "type":"petition",
         "title":"Remove Chuck Schumer and Nancy Pelosi from office",
         "body":"Schumer and Pelosi's hatred and refusing to work with...",
         "petition_type":[
          {
              "id":291,
              "name":"Call on Congress to act on an issue"
          }
        ],
        "signatureThreshold":100000,
        "status":"closed",
        "response":[

        ],
        "created":1547050064,
        "isSignable":false,
        "isPublic":true,
        "reachedPublic":0
       },

{ } Example

  "results":[
  {
     "id":"2722358",
     "type":"petition",
     "title":"Remove Chuck Schumer and Nancy Pelosi from office",
     "body":"Schumer and Pelosi's hatred and refusing to work with...",
     "petition_type":[
      {
          "id":291,
          "name":"Call on Congress to act on an issue"
      }
    ],
    "signatureThreshold":100000,
    "status":"closed",
    "response":{
         "id":2630367,
         "url":"https://petitions.whitehouse.gov/response/response-your-petition-3",
    },
    "created":1547050064,
    "isSignable":false,
    "isPublic":true,
    "reachedPublic":0
   },

From what I can tell from my research of the issue, JSON should be written consistently. In other words, since "response" is never an array, but just a single value then it should have been written as { } instead of [ ] when empty. Is my understanding of this correct?

Scooter
  • 4,068
  • 4
  • 32
  • 47
  • Hmm. It calls it valid. Even with one shown as [ ] and one shown as { } with stuff in it. Codable throws an error basically saying expected Array though. – Scooter Nov 29 '20 at 17:35
  • Thanks @dratenik. I cut the body down down here just for readability and forgot to add the quotes back. – Scooter Nov 29 '20 at 17:36
  • And, as always I appreciate the down vote...not sure why I come back to this site. – Scooter Nov 29 '20 at 17:38
  • Both those JSONs are probably fine, but as `[]` and `{...}` are different things, you will probably not be able to load both using one Codable definition. –  Nov 29 '20 at 17:49
  • Yeah, that is the same conclusion that I came to, but was hoping for a Stack Overflow miracle. I am unclear why they chose to switch them up like that. – Scooter Nov 29 '20 at 17:57
  • 1
    Not sure if this would help? https://stackoverflow.com/questions/52681385/swift-codable-multiple-types – Thomas Nov 29 '20 at 17:58
  • 1
    here's another article: https://medium.com/@sajjadsarkoobi/support-multiple-types-in-codable-swift-de461689e21b – Thomas Nov 29 '20 at 18:00
  • Alternatively you could pre-process the input to conform to what you want from it. Bash it into shape with a regex replacement? –  Nov 29 '20 at 18:01
  • "The downside of using Codable is that you can't encode and decode properties where the type is mixed or unknown, for example [String: Any], [Any] or Any. These are sometimes a neccessary evil in many apis, and AnyCodable makes supporting these types easy." https://github.com/yonaskolb/Codability#any-codable – Thomas Nov 29 '20 at 18:15

3 Answers3

1

If you are getting an empty array only when there is not value for response property, you can make response property optional, then implement the decoding and catch the DecodingError.typeMismatch error like this:

struct Results: Decodable {
    let results: [Result]
}

struct Result: Decodable {
    let id: String
    let type: String
    let title: String
    let body: String
    let petition_type: [PetitionType]
    let signatureThreshold: Int
    let status: String
    let response: Response?
    let created: Date
    let isSignable: Bool
    let isPublic: Bool
    let reachedPublic: Int
    
    enum CodingKeys: String, CodingKey {
        case id
        case type
        case title
        case body
        case petition_type
        case signatureThreshold
        case status
        case response
        case created
        case isSignable
        case isPublic
        case reachedPublic
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(String.self, forKey: .id)
        type = try container.decode(String.self, forKey: .type)
        title = try container.decode(String.self, forKey: .title)
        body = try container.decode(String.self, forKey: .body)
        petition_type = try container.decode([PetitionType].self, forKey: .petition_type)
        signatureThreshold = try container.decode(Int.self, forKey: .signatureThreshold)
        status = try container.decode(String.self, forKey: .status)
        
        do {
            response = try container.decode(Response.self, forKey: .response)
        } catch DecodingError.typeMismatch {
            response = nil
        }
        
        created = try container.decode(Date.self, forKey: .created)
        isSignable = try container.decode(Bool.self, forKey: .isSignable)
        isPublic = try container.decode(Bool.self, forKey: .isPublic)
        reachedPublic = try container.decode(Int.self, forKey: .reachedPublic)
    }
}

struct PetitionType: Decodable {
    var id: Int
    var name: String
}

struct Response: Decodable {
    let id: Int
    let url: String
}
gcharita
  • 7,729
  • 3
  • 20
  • 37
0

Personally, I would just create a decoding wrapper:

struct InvalidDecodableWrapper<T: Decodable>: Decodable {
    let value: T?

    init(from decoder: Decoder) throws {
        guard let container = try? decoder.singleValueContainer() else {
            value = nil
            return
        }

        value = try container.decode(T.self)
    }
}
struct Response: Decodable {
    let id: Int
    let url: String
}

struct Result: Decodable {    
    ...
    let response: InvalidDecodableWrapper<Response>
    ...
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270
0

Thanks for all the responses. I found the solution that worked for me here.

I created a new struct:

struct Response: Codable {
    var id: Int
    var url: String
}

Then created an extension of my Petition struct:

struct Petition {
    var title: String
    var body: String
    var signatureCount: Int
    var signatureThreshold: Int
    var signaturesNeeded: Int
    var status: String
    var response: [Response]
    var issues: Array<Issue>
 }

extension Petition: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        body  = try container.decode(String.self, forKey: .body)
        signatureCount = try container.decode(Int.self, forKey: .signatureCount)
        signatureThreshold = try container.decode(Int.self, forKey: .signatureThreshold)
        signaturesNeeded = try container.decode(Int.self, forKey: .signaturesNeeded)
        status = try container.decode(String.self, forKey: .status)
        do {
            response = try [container.decode(Response.self, forKey: .response)]
        } catch {
            response = try container.decode([Response].self, forKey: .response)
        }
        issues = try container.decode([Issue].self, forKey: .issues)
    }
}

The result is that I end up with an empty array, or an array with a single Response struct that I am able to access correctly. So in my viewController I simply check if response.count > 0 and if so it is safe to access response[0].url etc...

Scooter
  • 4,068
  • 4
  • 32
  • 47