Decode the included part from JSON API using the Codable protocol - json

the API which I am trying to consume in the iOS application which I am developing is the JSON API. I was using a library in order to parse the data which was being retrieved by the server. Now I am trying to parse the JSON on my own using the new Codable protocol which Swift introduced.
I successfully managed to parse the data and the attributes part but the difficulty I am facing is on the included part.
To start with I created this class:
class UserCodable: Codable {
var data: UserCodableData?
var relationships: UserRelationships?
enum CodingKeys: String, CodingKey {
case data
case relationships = "included"
}
}
in order to store the data retrieved for the User object.
Along with this class, I created these two structures
struct UserCodableData: Codable {
var id: String?
var type: String?
var attributes: UserAttributes?
}
struct UserAttributes: Codable {
var id: String?
var type: String?
var firstName: String?
var lastName: String?
var email: String?
var officePhone: String?
var mobilePhone: String?
var timeZone: String?
var active: Int?
var middleName: String?
var `extension`: String?
var homePhone: String?
var avatar: String?
enum CodingKeys: String, CodingKey {
case id
case type
case firstName = "firstname"
case lastName = "lastname"
case email
case officePhone = "office_phone"
case mobilePhone = "mobile_phone"
case timeZone = "time_zone"
case active
case middleName = "middlename"
case `extension`
case homePhone = "home_phone"
case avatar
}
}
in order to store the data and the attributes appropriately.
Finally I created the structure regarding the relationships (included):
struct UserRelationships: Codable {
var role: RoleCodable?
}
The RoleCodable class follows the same pattern.
The data retrieved regarding the included key is the following:
"data": {
},
"included": [
{
"id": "10",
"type": "roles",
"attributes": {
"title": "Role"
}
}
],
The problem is that the included part contains an array of JSON objects.
How can I decode and initialise the object in the UserRelationships class - in this case the role of type RoleCodable?

This is not elegant but it is how I am doing it, and it involves a manual init(from decoder: Decoder) function for UserDecodable. Basically I read in the 'included' section twice: the first time to gather type information and the second time to construct the relationships with the correct types.
I am using the key .included for "included" rather than .relationships since "relationships" occurs as another section within a JSON API response and might cause confusion.
Here's the container for type information that you can re-use for any included relationships:
struct ResourceIdentifierObject: Codable {
let type: String
let id: String
enum CodingKeys: String, CodingKey {
case type
case id
}
}
And this is the first read:
let resourceIds = try data.decode([ResourceIdentifierObject].self,
forKey: .included)
Second time around we iterate through the 'included' JSON array whilst checking the type in our ResourceIdentifierObject at the same index and build each relationship manually (I've added a comments relationship to demonstrate different relationships):
var included = try data.nestedUnkeyedContainer(forKey: .included)
while !included.isAtEnd {
let resourceId = resourceIds[included.currentIndex]
switch resourceId.type {
case "roles":
role = try included.decode(RoleCodable.self)
case "comments":
comments.append(try included.decode(CommentCodable.self))
default:
print("unexpected type")
}
}

Related

Augment struct parsed with json with variable [duplicate]

This question already has answers here:
When to use CodingKeys in Decodable(Swift)
(4 answers)
How to exclude properties from Swift Codable?
(7 answers)
Closed 4 months ago.
I have some static data for a list of "Badges" that I'm able to parse with JSONDecoder. I want to add a state that is not in the json data and when I added the variable to the Badge struct, parsing failed.
I then tried to create a two different structs, BadgeData which is parsed and Badge which contains state variable and BadgeData. But how do I merge this with state variable in a good way? Is there a better structure for this?
import Foundation
final class ModelData: ObservableObject {
#Published var badges: [Badge] = // ??
var badgeData: [BadgeData] = load("badges.json")
var taken: [Int] = load("taken.json")
}
struct BadgeData: Hashable, Codable, Identifiable {
let id: Int
let title: String
let imageRef: String
let requirements: [Requirement]
}
struct Badge: Hashable, Codable, Identifiable {
let data: BadgeData
var id: Int {
data.id
}
var isTaken: Bool
}
struct Requirement: Hashable, Codable, Identifiable {
var id: Int
let title: String
let detail: String
}
when I added the variable to the Badge struct, parsing failed.
You need coding keys.
Defining coding keys allows you to determine which properties you get from JSON and which are ignored
struct Badge: Hashable, Codable, Identifiable {
let id: Int
let title: String
let imageRef: String
let requirements: [Requirement]
var isTaken: Bool
enum CodingKeys: String, CodingKey
{
case id
case title
case imageRef
case requirements
}
}
The cases in CodingKeys map one to one to the properties you want to deserialise from the JSON.
Note also, that CodingKeys has a raw type which is String. This means that you can have different keys in your JSON to the property names e.g.
enum CodingKeys: String, CodingKey
{
case id
case title
case imageRef = "image_ref"
case requirements
}
would mean that the imageRef property in the object comes from the image_ref property in the JSON.

How to make a JSON model

I know you have to create a model of the expected JSON. I’m having trouble creating one from the Airtable API.
Here is my data:
{
"id": "recBOdydIpM2P3xkZ",
"createdTime": "2022-04-26T17:47:12.000Z",
"fields": {
"% GP": "32.31%",
"Total Rx": 103,
"Gross Profit": 1534.77,
"Total Received": 4749.55,
"Date": "2022-04-25",
"Copay": 2469.43,
"Acquisition Cost": 3214.78,
"Average GP": 14.9,
"TP Remitted": 2280.12
}
}
Here’s my model:
struct Record: Codable, Identifiable {
var id = UUID()
let createdTime: Date
let fields: //where im stuck :(
}
Make a new struct called Fields:
struct Record: Codable, Identifiable {
var id = UUID()
let createdTime: Date
let fields: Fields
struct Fields: Codable {
let gpPercent: String
let totalRx: Int
[...]
enum CodingKeys: String, CodingKey {
case gpPercent = "% GP"
case totalRx = "Total Rx"
[...]
}
}
enum CodingKeys: String, CodingKey {
case id
case createdTime
case fields
}
}
This is a broad question with multiple possible solutions. A model is basically what you want the objects you're building to look like. I would recommend looking into object oriented programming, like here: https://www.raywenderlich.com/599-object-oriented-programming-in-swift
Here is one possible solution:
struct Record: Codable, Identifiable {
var id = UUID
let createdTime: Date
let percentGP: Int
let totalRx: Int
let grossProfit: Double
let totalReceived: Double
let date: Date
let copay: Double
let acquisitionCost: Double
let averageGP: Double
let TPRemitted: Double
}
If you need to decode "fields" as a category into SwiftUI, you could create it as a separate object, like:
struct RecordData: Codable {
let percentGP: Int
// etc
}
and then, in Record:
struct Record: Codable, Identifiable {
var id = UUID
let createdTime: Date
let fields: RecordData
}
I am not using "Fields" as the name on purpose, to avoid confusion with plurals. You could use it, just be wary of using a single entity of Fields, not something like [Fields].

How to decode dictionary JSON response in Swift?

struct Chat: Codable, Identifiable {
let id = UUID()
var Messages: [Messages]
}
class ChatApi : ObservableObject{
#Published var chats = Chat()
func loadData(completion:#escaping (Chat) -> ()) {
let urlString = prefixUrl+"/room"
let url = NSURL(string: urlString as String)!
var request = URLRequest(url: url as URL)
request.setValue(accessKey, forHTTPHeaderField: "X-Access-Key-Id")
request.setValue(secretkey, forHTTPHeaderField: "X-Access-Key-Secret")
URLSession.shared.dataTask(with: request) { data, response, error in
let chats = try! JSONDecoder().decode(Chat.self, from: data!)
print(chats)
DispatchQueue.main.async {
completion(chats)
}
}.resume()
}
}
I'm not able to decode the following JSON response using Swift.
{
"Messages": [
{...}
]
}
I have tried the above ways and Xcode keeps throwing error. Although I'm able to decode JSON response with another function that are like this
[
{...},
{...},
{...}
]
I'm able to decode JSON response that are returned as arrays but not as dictionaries.
Example response to decode
{
"Messages": [
{
"_id": "MS4mMbTXok8g",
"_created_at": "2022-04-05T10:58:54Z",
"_created_by": {
"_id": "Us123",
"Name": "John Doe",
},
"_modified_at": "2022-04-05T10:58:54Z",
"Type": "Message",
"_raw_content": "ss",
"RoomId": "Ro1234",
},
{
"_id": "MS4m3oYXadUV",
"_created_at": "2022-04-04T15:22:21Z",
"_created_by": {
"_id": "Us678",
"Name": "Jim Lane",
},
"_modified_at": "2022-04-04T15:22:21Z",
"Type": "Message",
"_raw_content": "ss",
"RoomId": "Ro1234",
}
]
}
The data model that I've used is
struct CreatedBy: Codable {
var _id: String
var Name: String
}
struct Messages: Codable {
var _id: String
var _created_by: CreatedBy?
var `Type`: String?
var _raw_content: String
}
struct Chat: Codable, Identifiable {
let id = UUID()
var Messages: [Messages]
}
The error message before compilation is Editor placeholder in source file
I am going to introduce you to a couple of sites that will help when handling JSON decoding: JSON Formatter & Validator and Quicktype. The first makes sure that the JSON that you are working off of is actually valid, and will format it into a human readable form. The second will write that actual decodable structs. These are not perfect, and you may want to edit them, but they will get the job done while you are learning.
As soon as you posted your data model, I could see the problem. One of your variables is:
var `Type`: String?
The compiler is seeing the ` and thinking it is supposed to be a placeholder. You can't use them in the code.
Also, though there is not any code posted, I am not sure you need to make Chat Identifiable, as opposed to Messages which could be, but are not. I would switch, or at least add, Identifiable to Messages. I also made CreatedBy Identifiable since it also has a unique id.
The other thing you are missing which will make your code more readable is a CodingKeys enum. This translates the keys from what you are getting in JSON to what you want your variables to actually be. I ran your JSON through the above sites, and this is what came up with:
// MARK: - Chat
struct Chat: Codable {
let messages: [Message]
enum CodingKeys: String, CodingKey {
case messages = "Messages"
}
}
// MARK: - Message
struct Message: Codable, Identifiable {
let id: String
let createdAt: Date
let createdBy: CreatedBy
let modifiedAt: Date
let type, rawContent, roomID: String
enum CodingKeys: String, CodingKey {
case id = "_id"
case createdAt = "_created_at"
case createdBy = "_created_by"
case modifiedAt = "_modified_at"
case type = "Type"
case rawContent = "_raw_content"
case roomID = "RoomId"
}
}
// MARK: - CreatedBy
struct CreatedBy: Codable, Identifiable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id = "_id"
case name = "Name"
}
}
This gives you conventional Swift variables, as opposed to the snake case the JSON is giving you. Try this in your code and let us know if you have any more problems.

Is there a way to only partially create an object from JSON in Swift?

I'm creating a SwiftUI flashcard app, and I have no problem using Codable and following the technique Apple demonstrated with their landmarks tutorial app for importing JSON data in order to create their array of objects.
However, two of my flashcard objects' properties don't need to be loaded from JSON, and I could minimize the text needed in the JSON file if I could initialize those values separately instead of loading them from JSON. The problem is I cannot get JSON data to load without an error unless it maps exactly to ALL the object's properties, even if the missing properties are hardcoded with values.
Here is my object model:
import SwiftUI
class Flashcard: Codable, Identifiable {
let id: Int
var status: Int
let chapter: Int
let question: String
let answer: String
let reference: String
}
Here is JSON that works:
[
{
"id": 1,
"status": 0,
"chapter": 1,
"question": "This is the question",
"answer": "This is the answer",
"reference": "This is the reference"
}
//other card info repeated after with a comma separating each
]
Instead of having "id" and "status" listed unecessarily in the JSON, I would prefer to change the model to something like this:
import SwiftUI
class Flashcard: Codable, Identifiable {
let id = UUID()
var status: Int = 0
//only load these from JSON:
let chapter: Int
let question: String
let answer: String
let reference: String
}
...and then I theoretically should be able to eliminate "id" and "status" from the JSON (but I can't). Is there a simple way to do this that prevents the error from JSON not mapping completely to the object?
Yes, you can do this by setting the coding keys on your Codable class. Just leave out the ones that you don't want to update from the json.
class Flashcard: Codable, Identifiable {
let id = UUID()
var status: Int = 0
let chapter: Int
let question: String
let answer: String
let reference: String
enum CodingKeys: String, CodingKey {
case chapter, question, answer, reference
}
}
There is a great article by HackingWithSwift on Codable here
you can use CodingKeys to define what fields to extract from the JSON.
class Flashcard: Codable, Identifiable {
enum CodingKeys: CodingKey {
case chapter
case question
case answer
case reference
}
let id = UUID()
var status: Int = 0
//only load these from JSON:
let chapter: Int
let question: String
let answer: String
let reference: String
}
The docuemntation has a good explanation (for once) of this under `Encoding and Decoding Custom Types`

How to parse several hardcoded keys in JSON API struct with Swift Decodable protocol?

Question: I am trying to decode my JSON, which some of the JSON will have a random string and some will have a hardcoded string. When the hardcoded string is one of the below, I would like to display different UICollectionView Cells. I am having trouble trying to parse my JSON if it is a hardcoded string and being able to display a different UICollectionViewCell with it. Any help on this would be great. This may be a beginner question but I have tried to solve this for this past week and I am having trouble trying to do it. Any help on this would be much appreciated.
** Hardcoded Strings that could be one or the other:**
key: --> This string could be "breaking" or "normal" or "new"
item: --> This string could be "placement" or "slot" or "shared"
verb: --> This string could be "shared" or "posted"
** NOT hardcoded strings, which the string comes in randomly**
heading: --> This string is a random string
type: --> This string is a random string
Here is some of my JSON, so you can get an example of what I am trying to do:
{
slots: [
{
key: "breaking",
item: "placement",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "shared"
sort_order: 0
},
{
key: "breaking",
item: "placement",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "posted"
sort_order: 1
},
{
key: "event",
item: "combine",
heading: "Random String Text",
type: "Random String Text",
via: "Random",
verb: "posted"
sort_order: 2
},
}
This is what I have so far for my model:
struct MyModel: Decodable {
var key: String?
var item: String?
var heading: String?
var type: String?
var via: String?
var verb: String?
}
Here is an example cell that Dmitry Serov helped me with.
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = ... // retrieve your model object here
if model.verb == .shared {
// Pass the pertinent identifier
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:...)
return cell
else {
....
}
}
Here is more code that Dmitry Serov helped me with.
struct MyModel { // Type names should use pascal case in Swift
var verb: State?
....
enum State {
case shared
case posted
}
}
// Decode your enums from strings
extension MyModel.State: Decodable {
enum CodingKeys: String, CodingKey {
case shared
case posted
}
}
The issue when I try the above it is asking me to put in the format below which I am not sure how to do and I am trying to parse out several more keys.
extension MyModel.State: Decodable {
init(from decoder: Decoder) throws {
}
}
this is the way I would decode your json using codable:
struct Response: Codable {
var slots = [Slots]()
}
struct Slots: Codable {
var key: String?
var item: String?
var heading: String?
var type: String?
var via: String?
var verb: String?
var order: String?
enum CodingKeys: String, CodingKey {
case order = "sort_order"
}
/** Custom Encoder to send null values**/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(order, forKey: .order)
}
}
To compare with hardcoded strings create an enum:
enum key: String {
case breking
case normal
case new
}
use it like this:
if let response = try? JSONDecoder().decode(Response.self, from: response.data!){
for slot in response.slots{
if slot.key == key.breaking.rawValue{
//do something
}
}
}