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) }
}
Related
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.
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 :)
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`
This is my first time taking a shot at Codable/Decodable and i would like to decode a JSON. I am attempting to access the "name" and "description" keys within the events array. Below is a snippet of the JSON - im getting this error within my code
"Expected to decode Dictionary but found an array instead."
"pagination": {
"page_number": 1,
"page_size": 50,
"continuation": "eyJwYWdlIjogMn0",
"has_more_items": true
},
"events": [
{
"name": {
"text": "Genesis Part 4",
"html": "Genesis Part 4"
},
"description": {
"text": "Wednesday, June 6-June 27, 2018\n12:00-2:15 PM, Eastern Time\n\u00a0\nCOED\n\u00a0\nBible Study Leader:\u00a0Nicki Cornett\n\u00a0\nContact:NickiCornett#gmail.com\n\u00a0\nGenesis Part 4 -\u00a0Wrestling with God - A Study on Isaac, Jacob, and Esau- Precept Workbook (NASB)\n\u00a0\nGod renews His covenant promise with Abraham through Isaac and Jacob.
Here is how i went about decoding - (NOTE - "description" is not here yet because i having an issue working into the events array to access name & descritption
struct Eventbrite: Decodable {
private enum CodingKeys : String, CodingKey { case events = "events", name = "name"}
let events: [String:[String]]
let name: [String:[String]]
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
do {
let eventbriteData = try JSONDecoder().decode(Eventbrite.self, from: data)
print(eventbriteData.name)
name is clearly not in the scope of pagination and events (note the {}) and is a regular [String:String] dictionary which can be decoded into another struct.
Decode this (as description is incomplete I left it out), you don't need CodingKeys:
struct Eventbrite: Decodable {
let events: [Event]
}
struct Event: Decodable {
let name: Name
// let description: [String:String]
}
struct Name : Decodable {
let text, html : String
}
Right so Decodable is actually pretty smart in that you really don't need to write any code to do the decoding yourself. You just have to make sure you match the JSON structure (and make structs that also conform to Decodable for any nested objects). In other words, instead of having variables as dictionaries, make them their own Decodable struct.
So for example:
struct EventBrite: Decodable {
let pagination: Pagination
let events: [Event]
}
struct Pagination: Decodable {
let page_number: Int
let page_size: Int
let continuation: String
let has_more_items: Bool
}
struct Event: Decodable {
let name: EventName
let description: EventDescription
}
struct EventName: Decodable {
let name: String
let html: String
}
etc...
Something else that's important here is if a key or property is not guaranteed to be returned (like let's say that the EventName doesn't always have an html value that comes back from the server you can easily just mark that value as optional. So something like:
struct EventName: Decodable {
let name: String
let html: String?
}
Another side note, you actually messed up your dictionary type declarations. You'll notice that event is actually of type [String: [String: String]] since the key is a string and the values seem to always be dictionary. And name is [String: String]. Which is not what you had them down as in your original question.
When the values can be different like with pagination you'll want to do something like [String: Any] so just be careful about that.
HOWEVER The approach I suggested I think is better than having properties be dictionaries. For one you don't have to worry about declaring the type of dictionary it is (which you made some small errors on). But more importantly when each dictionary just becomes its own clearly defined struct and you don't have to worry about remembering or looking up the keys. Dot syntax/auto complete will automatically tell you what there can be! (And no casting when your value is of type Any or AnyObject!)
Also definitely use structs for all these as I once benchmarked performance and measured structs efficiency on the order of magnitude of millions of times more efficient than classes. Just a FYI.
I need to "replicate" an entiry which is returned from a remote web API service in JSON. It looks like this:
{
"field1": "some_id",
"entity_name" = "Entity1"
"field2": "some name",
"details1": [{
"field1": 11,
"field2": "some value",
"data": {
"key1": "value1",
"key2": "value2",
"key3": "value3",
// any other, unknown at compile time keys
}
}],
"details2": {
"field1": 13,
"field2": "some value2"
}
}
Here's my attempt:
struct Entity1 {
struct Details1 {
let field1: UInt32
let field2: String
let data: [String: String]
}
struct Details2 {
let field1: UInt32
let field2: String
}
let field1: String
static let entityName = "Entity1"
let field2: String
let details1: [Details1]
let details2: Details2
}
Is it a good idea to use structs instead of classes for such a goal
as mine?
Can I anyhow define a nested struct or a class, say
Details1 and create a variable of it at the same time?
Like this:
//doesn't compile
struct Entity1 {
let details1: [Details1 {
let field1: UInt32
let field2: String
let data: [String: String]
}]
You can use any if the following good open-source libraries available to handle the mapping of JSON to Object in Swift, take a look :
Mapper
ObjectMapper
JSONHelper
Argo
Unbox
Each one have nice a good tutorial for beginners.
Regarding the theme of struct or class, you can consider the following text from The Swift Programming Language documentation:
Structure instances are always passed by value, and class
instances are always passed by reference. This means that they are
suited to different kinds of tasks. As you consider the data
constructs and functionality that you need for a project, decide
whether each data construct should be defined as a class or as a
structure.
As a general guideline, consider creating a structure when one or more
of these conditions apply:
The structure’s primary purpose is to encapsulate a few relatively simple data values.
It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an
instance of that structure.
Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
The structure does not need to inherit properties or behavior from another existing type.
Examples of good candidates for structures include:
The size of a geometric shape, perhaps encapsulating a width property and a height property, both of type Double.
A way to refer to ranges within a series, perhaps encapsulating a start property and a length property, both of type Int.
A point in a 3D coordinate system, perhaps encapsulating x, y and z properties, each of type Double.
In all other cases, define a class, and create instances of that class
to be managed and passed by reference. In practice, this means that
most custom data constructs should be classes, not structures.
I hope this help you.
HandyJSON is exactly what you need. See code example:
struct Animal: HandyJSON {
var name: String?
var id: String?
var num: Int?
}
let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"
if let animal = JSONDeserializer.deserializeFrom(json: jsonString) {
print(animal)
}
https://github.com/alibaba/handyjson
Details
Xcode 10.2.1 (10E1001), Swift 5
Links
Pods:
Alamofire - loading data
More info:
Codable
More samples of usage Codable and ObjectMapper in Swift 5
Task
Get itunes search results using iTunes Search API with simple request https://itunes.apple.com/search?term=jack+johnson
Full sample
import UIKit
import Alamofire
// Itunce api doc: https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#searching
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
private func loadData() {
let urlString = "https://itunes.apple.com/search?term=jack+johnson"
Alamofire.request(urlString).response { response in
guard let data = response.data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(ItunceItems.self, from: data)
print(result)
} catch let error {
print("\(error.localizedDescription)")
}
}
}
}
struct ItunceItems: Codable {
let resultCount: Int
let results: [ItunceItem]
}
struct ItunceItem: Codable {
var wrapperType: String?
var artistId: Int?
var trackName: String?
var trackPrice: Double?
var currency: String?
}
you could use SwiftyJson and let json = JSONValue(dataFromNetworking)
if let userName = json[0]["user"]["name"].string{
//Now you got your value
}
Take a look at this awesome library that perfectly fits your need, Argo on GitHub.
In your case, a struct is ok. You can read more on how to choose between a struct and a class here.
You can go with this extension for Alamofire https://github.com/sua8051/AlamofireMapper
Declare a class or struct:
class UserResponse: Decodable {
var page: Int!
var per_page: Int!
var total: Int!
var total_pages: Int!
var data: [User]?
}
class User: Decodable {
var id: Double!
var first_name: String!
var last_name: String!
var avatar: String!
}
Use:
import Alamofire
import AlamofireMapper
let url1 = "https://raw.githubusercontent.com/sua8051/AlamofireMapper/master/user1.json"
Alamofire.request(url1, method: .get
, parameters: nil, encoding: URLEncoding.default, headers: nil).responseObject { (response: DataResponse<UserResponse>) in
switch response.result {
case let .success(data):
dump(data)
case let .failure(error):
dump(error)
}
}