This question already has answers here:
Swift Codable multiple types
(2 answers)
Closed 1 year ago.
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?
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
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?
....
}
Related
How can I implement a solution where I could set the type of a parameter on the go while parsing a JSON by analyzing the parameter at the same level as the other parameter?
let sampleData = Data("""
[
{
"type": "one",
"body": {
"one": 1
},
.
.
.
},
{
"type": "two",
"body": {
"two": 2,
"twoData": "two"
},
.
.
.
}
]
""".utf8)
struct MyObject: Codable {
let type: String
let body: Body
}
struct Body: Codable {
let one, two: Int?
let twoData: String?
}
print(try JSONDecoder().decode([MyObject].self, from: sampleData))
Here you can see that the keys in Body are all optionals. My solution requires they being parsed into different types according to the value given in the parameter type. How can I parse body into the following 2 separate types according to the value I receive in type?
struct OneBody: Decodable {
let one: Int
}
struct TwoBody: Decodable {
let two: Int
let twoData: String
}
The approach that I finally went with is with a custom init(from decoder: Decoder) method where the type is determined first and then using a switch statement iterate through each case to decode each type and assign it to the body as another enum. Here is the code:
struct MyObject: Decodable {
let body: MyBody
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(MyType.self, forKey: .type)
switch type {
case .one:
body = .one(try container.decode(OneBody.self, forKey: .body))
case .two:
body = .two(try container.decode(TwoBody.self, forKey: .body))
}
}
enum CodingKeys: String, CodingKey {
case type, body
}
}
enum MyType: String, Decodable {
case one, two
}
enum MyBody: Decodable {
case one(OneBody)
case two(TwoBody)
}
struct OneBody: Decodable {
let one: Int
}
struct TwoBody: Decodable {
let two: Int
let twoData: String
}
print(try JSONDecoder().decode([MyObject].self, from: sampleData))
PS: Please post an answer or a comment if there are any ideas for a better approach.
My problem is that I have JSON object got from the server like this one:
{
"data": [
{
"id": 1,
"name": "at",
"amount": 446,
"createdAt": "25/04/2020",
"updatedAt": "25/04/2020"
},
{
"id": 2,
"name": "iste",
"amount": 872,
"createdAt": "25/04/2020",
"updatedAt": "25/04/2020"
}
]
}
And I have Codable struct that decodes this object:
struct Expense: Codable, Identifiable {
var id: Int
var name: String
var amount: String
var createdAt: String
var updatedAt: String
}
Also, I have class with a static method that will do the AF request, also I'm using FuturePromise library for hendling completionof the request:
struct RequestAPI {
#discardableResult
static func callAndDecode<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder()) -> Future<T> {
return Future(operation: { completion in
AF.request(route).responseDecodable(decoder: decoder, completionHandler: { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
print(error.localizedDescription)
completion(.failure(error))
}
})
})
}
}
Problem is that I have a root "data" parameter that sometimes is present and sometimes not.
I know that there is a solution that I can create a Result codable Model that will be the parent of the Expense Model, but that does not approach that I want, because what will happen if I will have 20 different models I'll have to create 20 deferent root Models?
Yes, I can do it with CodingKeys but that is a little bit hacky and too much of code for this simple task.
So the best approach is to add something like this:
struct ExpensesList: Codable {
var data: [Expense]
}
But for me, it is a problem that I will always have 'data' root, so then for any model I will have some 'List' model.
Is there a better approach that is not hacky or this is the only one.
Maybe to send a child model to one data model, but how to recognize it in views,...?
Thank you in advance.
If I understand you correctly, you can make your Root structure generic, and should also make the data property optional.
struct Foo: Decodable {
let foo: Int
}
struct Bar: Decodable {
let bar: String
}
struct Root<T: Decodable>: Decodable {
let data: [T]?
}
typealias FooList = Root<Foo>
typealias BarList = Root<Bar>
let fooData = """
{ "data": [ { "foo": 42 } ] }
""".data(using: .utf8)!
let barData = """
{ "data": [ { "bar": "baz" } ] }
""".data(using: .utf8)!
do {
let foos = try JSONDecoder().decode(FooList.self, from: fooData)
let bars = try JSONDecoder().decode(BarList.self, from: barData)
} catch {
print(error)
}
I don’t know how to handle a JSON feed in swift with the decodable protocol when the feed gives me two different results. If there is more than one ‘entry’, the json looks like this and the entry value is an array of objects
{
"feed": {
"publisher": "Penguin",
"country": "ca"
},
"entry": [
{
"author": "Margaret Atwood",
"nationality": "Canadian"
},
{
"author": "Dan Brown",
"nationality": "American"
}
]
}
However, if there is only a single entry, the json looks like this where entry is just a dictionary
{
"feed": {
"publisher": "Penguin",
"country": "ca"
},
"entry": {
"author": "Margaret Atwood",
"nationality": "Canadian"
}
}
to decode the first case, I would use the following structs
struct Book: Decodable {
let feed: Feed
let entry: [Entry]
}
// MARK: - Entry
struct Entry: Decodable {
let author, nationality: String
}
// MARK: - Feed
struct Feed: Decodable {
let publisher, country: String
}
And then use something like this to decode the data retrieved
let object = try JSONDecoder().decode(Book.self, from: data)
How do I handle the case when the entry is not an array of objects?
You could possibly override the decoder for Book. What you can do is to try to unwrap an [Entry]s and upon failing that, just try to unwrap a single Entry.
For example:
struct Book: Decodable {
let feed: Feed
let entry: [Entry]
init (from decoder :Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode([Entry].self, forKey: .value)
} catch {
let newValue = try container.decode(Entry.self, forKey: .value)
value = [newValue]
}
}
}
Note: that's not a comprehensive sample on what you want to do but just a way you can accomplish what you want to do
I'm trying to call API using Codable and i want to access all dictionary, arrays from API.
Is This Possible from codable?
API Response eg:
{
"status": true,
"logo": "https://abc.png",
"data": [
{
"crumb": {
"Menu": {
"navigate": "Home",
},
},
"path": "2",
"type": "type0",
"orientation": [
{
"name": "All",
}
],
},
]
}
The API response you've posted is invalid JSON (it has a bunch of trailing commas that make it illegal). This needs to be changed on the producer's side, and when you've done that, you can use this struct to access the data:
struct Entry: Codable {
let status: Bool
let logo: String
let data: [Datum]
}
struct Datum: Codable {
let crumb: Crumb
let path, type: String
let orientation: [Orientation]
}
struct Crumb: Codable {
let menu: Menu
enum CodingKeys: String, CodingKey {
case menu = "Menu"
}
}
struct Menu: Codable {
let navigate: String
}
struct Orientation: Codable {
let name: String
}
I have a below JSON response. And I want to fetch the allkeys of “data” which is [“rules”, “preference”, “goals”] using .keys method. But I couldn’t get the array of allkeys using .keys feature. I have attached my code snippet also. if you had faced this one, please suggest me to rid of this concern.
Although, I can get these allKeys using ObjectMapper and native Dictionary objects. I just need to know why I couldn't achieve this using Codable.
My json response
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
My code Snippet:
struct MeetingsDataModal: Codable {
let statusCode: Int?
let status: String?
let message: String?
let data: Results?
enum CodingKeys: String, CodingKey {
case statusCode = "statusCode"
case status = "status"
case message = "message"
case data = "data"
}
func allkeys() {
}
}
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
}
struct Rules : Codable {
}
struct Preference : Codable {
}
struct Goals : Codable {
}
My expectation
let content = try JSONDecoder().decode(MeetingsDataModal.self, from: (response as? Data)!)
print(content.data.keys)
But I am getting,
Value of type 'Results?' has no member 'keys'
Perhaps I am not understanding the question well but your "keys" are defined by your Codable protocol - so they are known. If you are using Swift 4.2+ you can take advantage of the CaseIterable protocol
struct Results: Codable {
let testOne: Int
let testTwo: String
enum CodingKeys: String, CodingKey, CaseIterable {
case testOne
case testTwo
}
}
Results.CodingKeys.allCases.map { $0.rawValue }
If you do need .keys. could add two-line code :
struct Results : Codable {
let rules: Rules?
let preference: [Preference]?
let goals: Goals?
enum CodingKeys: String, CodingKey {
case rules = "rules"
case preference = "preference"
case goals = "goals"
}
var keys : [String]{
return["rules", "preference","goals"]
}
}
Actually, if you don't like encoding/decoding way , we have another traditional JSON object can help you to handle unstructured JSON.
let obj = try JSONSerialization.jsonObject(with: response!, options: JSONSerialization.ReadingOptions.allowFragments)
print((((obj as! [String: Any?])["data"]) as! [String: Any?]).keys)
Here is one way to decode you data structure.
let d = """
{
"statusCode": 200,
"status": "success",
"message": null,
"data": {
"rules": {
"goals": {
"min": "1",
"max": "3"
}
},
"preference": [
1,
2,
3
],
"goals": {
"total": 4,
"data": []
}
}
}
""".data(using: .utf8)!
struct MeetingsDataModal: Decodable {
let statusCode: Int
let status: String
let message: String?
let data: Results
}
struct Results : Decodable {
let rules: Rules
let preference: [Int]
let goals: Goals
}
struct Rules : Decodable {
let goals : DirectData
}
struct DirectData : Decodable{
let min : String
let max : String
}
struct Goals : Decodable {
let total : Int
let data : [String]
}
let data0 = try JSONDecoder().decode(MeetingsDataModal.self, from: d)
print(data0)