Augment struct parsed with json with variable [duplicate] - json

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.

Related

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

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.

Parsing multiple JSON files with Swift using one Decodable Struct

I have several JSON feeds, for this example let's these two (A & B). Both have different structures but both have an array of structures identifying the elements I'd like to parse, the bike stations.
I'd like to avoid creating a different class for each JSON feed I have to parse and if possible parse the underlying array of structures with the same Decodable struct. The definition of my model is as follows,
struct Places: Decodable {
var name: String
let lat: Double
let lng: Double
let id: String
let bikes: Int
enum CodingKeys: String, CodingKey {
case name
case lat = "latitude"
case lng = "longitude"
case id
case bikes = "free_bikes"
}
}
This model would only serve for one JSON feed and for each I'd have to create different CodingKeys. This is also a problem because the intermediate elements differ from feed to feed.
What I currently have is different parsers used on each feed. My app uses the array of Places to add pins on a map so the defined struct has to be the same for every feed that I parse. This is not a scalable solution for me and I'd like to ask if the following is correct,
Can I have only one parser that fit all my needs?
Can I have only one parser with different root elements and the same Places struct in the end?
Can I build a parser that only accesses the intermediate elements defined in the Places structure and "forget" about the top level differences between the feeds
I asked a similar question here only for the inner elements. While that is still true I now have problems parsing all the documents only to get the array of Places.
One thing right off the top, I would suggest rename your Places struct to Place (singular). I would also suggest using the full names for latitude and longitude.
On to the specific questions:
I think 1 & 2 would be possible, but you might end up with much less maintainable code.
Yes. Doable with the caveat above.
Yes. Challenging, because in the NextBike & CitiBike examples you provided, the IDs are of different types (String id in CitiBike and Int uid), so would need to be mapped.
No, I don't think so, because you'll still need to do the type mapping mentioned above in 2.
For maintainability, I would suggest creating Decodable structs/classes for each of the types of feeds with (possibly) embedded structs/classes to support hierarchy and then provide a map to [Place]. You can use a protocol to enforce compliance.
protocol PlacesProviding {
var places: [Place] { get }
}
struct Place: Decodable {
var name: String
let latitude: Double
let longitude: Double
let id: String
let bikes: Int
Using your NextBike & CitiBike examples, you might want to be able to do something like:
let decoder = JSONDecoder()
let nyc = decoder.decode(NYCitiBike.self, from: citiBikeNYC)
let nb = decoder.decode(NextBike.self, from: nextBike)
var places = [Place]()
places.append(contentsOf: nb.places)
places.append(contentsOf: nyc.places)
print(places.count)
Here are example Decodable PlaceProviding objects that support this. The structure of these make very clear the expected structure of the associated feed and make it easy to surface additional properties from a feed. This improves maintainability, especially in the longer term.
struct NYCitiBike: Decodable, PlacesProviding {
struct Network: Decodable {
let stations: [Station]
}
struct Station: Decodable {
var name: String
let latitude: Double
let longitude: Double
let id: String
let bikes: Int
enum CodingKeys: String, CodingKey {
case name
case latitude
case longitude
case id
case bikes = "free_bikes"
}
}
let network: Network
var stations: [Station] { return network.stations }
var places: [Place] {
stations.map { Place(name: $0.name, latitude: $0.latitude, longitude: $0.longitude, id: $0.id, bikes: $0.bikes) }
}
struct NextBike: Decodable, PlacesProviding {
struct Country: Decodable {
let name: String
let cities: [City]
}
struct City: Decodable {
let name: String
let places: [Station]
}
struct Station: Decodable {
let name: String
let lat: Double
let lng: Double
let uid: Int
let bikes: Int
}
let countries: [Country]
var stations: [Station] {
return countries
.flatMap { $0.cities }
.flatMap { $0.places }
}
var places: [Place] {
stations.map { Place(name: $0.name, latitude: $0.lat, longitude: $0.lng, id: String($0.uid), bikes: $0.bikes) }
}

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`

swift 4 - Parsing json to struct (codable), but with an extra variable which is not from json

I am currently getting all the data I want from my json, but I want to add extra variable which is not from the json.
struct LoanUser: Codable {
let name: String
let use: String
let location: Location
let loan_amount: Int
let image: Image
var favorite: Bool = false
}
The var favorite: bool = false is not a json string. This is the extra variable I want added
You have to specify coding keys by yourself and do not include favorite
struct LoanUser: Codable {
let name: String
let use: String
let location: Location
let loan_amount: Int
let image: Image
var favorite: Bool = false
enum CodingKeys: String, CodingKey {
case name, use, location, loan_amount, image
}
}