0

I have a request object that points to an API from Stripe. The response from stripe looks like this

{
  "object": "list",
  "url": "/v1/refunds",
  "has_more": false,
  "data": [
    {
      "id": "re_3Jkggg2eZvKYlo2C0ArxjggM",
      "object": "refund",
      "amount": 100,
      "balance_transaction": null,
      "charge": "ch_3Jkggg2eZvKYlo2C0pK8hM73",
      "created": 1634266948,
      "currency": "usd",
      "metadata": {},
      "payment_intent": null,
      "reason": null,
      "receipt_number": null,
      "source_transfer_reversal": null,
      "status": "succeeded",
      "transfer_reversal": null
    },
    {...},
    {...}
  ]
}

I've created a decodable Request struct that looks like this:

struct Request: Decodable {
    var object: String
    var url: String
    var has_more: Bool
    var data: [Any]
}

The issue I am having is that the Request object can have data that contains an array of several different Objects. A refund object, a card object, and others. Because of this, I've added [Any] to the request struct but am getting this error:

Type 'Request' does not conform to protocol 'Decodable'

This seems to be because decodable can't use Any as a variable type. How can I get around this and use a universal Request object with dynamic object types?

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • You can't use `Any` as Array Element. Your Array Element has conform to `Codable`. You will need to implement a custom struct that conforms to Decodable for your inner `data` object. – Rob C Oct 15 '21 at 03:56
  • instead of writing any you have to make another struct named Data with add decodable and write all the above properties of data – Zeeshan Ahmad II Oct 15 '21 at 04:39
  • You can create your Swift model/struct using below website https://app.quicktype.io/ – Umair Ahmed Oct 15 '21 at 05:00
  • struct Request: Codable { let object, url: String let hasMore: Bool let data: [MultipleData] enum CodingKeys: String, CodingKey { case object, url case hasMore = "has_more" case data } } – Umair Ahmed Oct 15 '21 at 05:03
  • It's good to try yourself for "MultipleData" Struct – Umair Ahmed Oct 15 '21 at 05:05

2 Answers2

2

You could utilize enum to solve your problem, I have done this in the past with great success.

The code example below can be copy/pasted into the Playground for further tinkering. This is a good way increase your understanding of the inner workings when decoding JSON to Decodable objects.

Hope this can nudge you in a direction that will work in your case.

//: A UIKit based Playground for presenting user interface

import PlaygroundSupport
import Foundation

let json = """
{
    "object": "list",
    "url": "/v1/refunds",
    "has_more": false,
    "data": [
        {
            "id": "some_id",
            "object": "refund",
            "amount": 100,
        },
        {
            "id": "some_other_id",
            "object": "card",
            "cardNumber": "1337 1447 1337 1447"
        }
    ]
}
"""

struct Request: Decodable {
    let object: String
    let url: String
    let has_more: Bool
    let data: [RequestData] // A list of a type with a dynamic data property to hold object specific information.
}

// Type for objects of type 'card'.
struct Card: Decodable {
    let cardNumber: String
}

// Type for objects of type 'refund'.
struct Refund: Decodable {
    let amount: Int
}

// A simple enum for every object possible in the 'data' array.
enum RefundObject: String, Decodable {
    case card
    case refund
}

// An enum with associated values, mirroring the cases from RefundObject.
enum RefundData: Decodable {
    case card(Card)
    case refund(Refund)
}

// The base data object in the 'data' array.
struct RequestData: Decodable {
    let id: String // Common properties can live in the outer part of the data object.
    let object: RefundObject
    let innerObject: RefundData // An enum that contain any of the cases defined within the RefundData enum.

    enum CodingKeys: String, CodingKey {
        case id
        case object
        case data
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.object = try container.decode(RefundObject.self, forKey: .object)

        // Here we decode id (I assumed it was a common property), and object.
        // The object will be used to determine what the inner part of the data object should be.

        switch object {
        case .card:
            // Set innerObject to the .card case with an associated value of Card.
            self.innerObject = .card(
                try Card(from: decoder)
            )
        case .refund:
            // Set innerObject to the .refund case with an associated value of Refund.
            self.innerObject = .refund(
                try Refund(from: decoder)
            )
        }
    }
}

let decoder = JSONDecoder()
let requestData = try decoder.decode(Request.self, from: json.data(using: .utf8)!)

// Example usage of the innerObject:
for data in requestData.data {
    switch data.innerObject {
    case .card(let card):
        print(card.cardNumber)
    case .refund(let refund):
        print(refund.amount)
    }
}

https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html

https://developer.apple.com/documentation/foundation/jsondecoder

Laffen
  • 2,753
  • 17
  • 29
1
struct Request: Decodable {
  var object: String
  var url: String
  var has_more: Bool
  var data: [MyData]
}

then you have also make a decodable struct for that data array

struct MyData:Decodable {
  var id: String
  var object: String
  var amount:Int
  balance_transaction:String?
 ....
   
}
Zeeshan Ahmad II
  • 1,047
  • 1
  • 5
  • 9