How to make a JSON model - json

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].

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.

parsing nested JSON in Swift 5

Can't figure out how to build the struct for this nested JSON. I'm so close but missing something..
I'm trying to verify I'm loading correctly... the first two work great the nested data fails
print(json.pagination.items) // this works
print(json.releases[3].date_added) // this works
print(json.releases[3].basicInformation?.year) //NOT WORKING, returns nil
here is the struct in built
struct Response: Codable {
let pagination: MyResult
let releases: [MyReleases]
}
struct MyResult: Codable {
var page: Int
var per_page: Int
var items: Int
}
struct MyReleases: Codable {
var date_added: String
let basicInformation: BasicInformation?
}
struct BasicInformation: Codable {
let title: String
let year: Int
enum CodingKeys: String, CodingKey {
case title, year
}
}
My JSON is
{
"pagination":{
"page":1,
"pages":2,
"per_page":50,
"items":81,
"urls":{
"last":"https://api.discogs.com/users/douglasbrown/collection/folders/0/releases?page=2&per_page=50",
"next":"https://api.discogs.com/users/douglasbrown/collection/folders/0/releases?page=2&per_page=50"
}
},
"releases":[
{
"id":9393649,
"instance_id":656332897,
"date_added":"2021-03-28T10:54:09-07:00",
"rating":2,
"basic_information":{
"id":9393649,
"master_id":353625,
"master_url":"https://api.discogs.com/masters/353625",
"resource_url":"https://api.discogs.com/releases/9393649",
"thumb":"",
"cover_image":"",
"title":"Ten Summoner's Tales",
"year":2016
}
}
]
}
Any help would be greatly appreciated. I'm so close but missing something... :(
First of all
json.releases[3].date_added
doesn't work with the given JSON, it will crash because there is only one release.
In MyReleases you have to add CodingKeys to map the snake_case name(s)
struct MyReleases: Codable {
var dateAdded: String
let basicInformation: BasicInformation?
private enum CodingKeys: String, CodingKey {
case basicInformation = "basic_information", dateAdded = "date_added"
}
}
or add the .convertFromSnakeCase key decoding strategy.
You can even decode dateAdded as Date with the .iso8601 date decoding strategy.

Swift 5 Parsing strange json format

I'm trying to parse JSON but keep getting incorrect format error. The JSON I get back from FoodData Central (the USDA's Nutrition API) is as follows:
{
dataType = "Survey (FNDDS)";
description = "Chicken thigh, NS as to cooking method, skin not eaten";
fdcId = 782172;
foodNutrients = (
{
amount = "24.09";
id = 9141826;
nutrient = {
id = 1003;
name = Protein;
number = 203;
rank = 600;
unitName = g;
};
type = FoodNutrient;
},
{
amount = "10.74";
id = "9141827";
nutrient = {
id = 1004;
name = "Total lipid (fat)";
number = 204;
rank = 800;
unitName = g;
};
type = FoodNutrient;
}
);
}
My Structs:
struct Root: Decodable {
let description: String
let foodNutrients: FoodNutrients
}
struct FoodNutrients: Decodable {
// What should go here???
}
From the JSON, it looks like foodNutrients is an array of unnamed objects, each of which has the values amount: String, id: String, and nutrient: Nutrient (which has id, name etc...) However, forgetting the Nutrient object, I can't even parse the amounts.
struct FoodNutrients: Decodable {
let amounts: [String]
}
I don't think its an array of string, but I have no idea what the () in foodNutrients would indicate.
How would I go about parsing this JSON. I'm using Swift 5 and JSONDecoder. To get the JSON I use JSONSerializer, then print out the JSON above.
This is not a JSON. This is a property list in the openStep format.
This is how it can be modelled (use String instead of Int):
struct Root: Decodable {
let description: String
let foodNutrients: [FoodNutrient]
}
struct FoodNutrient: Decodable {
let id: String
let amount: String
let nutrient: Nutrient
}
struct Nutrient: Decodable {
let name: String
let number: String
let rank: String
let unitName: String
}
And then decode it like this:
try PropertyListDecoder().decode(Root.self, from: yourStr)
The () in foodNutrients indicates that it holds an array of objects - in that case FoodNutrient objects. Therefore your root object should look like this:
struct Root: Decodable {
let description: String
let foodNutrients: [FoodNutrient]
}
Now the foodNutrient is except for the nutrient object straightforward:
struct FoodNutrient: Decodable {
let id: Int // <-- in your example it is an integer and in the second object a string, choose the fitting one from the API
let amount: String
let nutrient: Nutrient
}
And the nutrient object should look like this:
struct Nutrient: Decodable {
let name: String
let number: Int
let rank: Int
let unitName: String
}
Using Decodable is a good and easy way to serialize JSON. Hope that helps. Happy coding :)

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`

Decode the included part from JSON API using the Codable protocol

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")
}
}