This question already has an answer here:
How can I decode a JSON response with an unknown key in Swift?
(1 answer)
Closed 3 years ago.
The JSON Data from API contains price for bitcoin in various currencies. I need to modify my struct at runtime, so that it matches up with the keys in data.
Init, Type alias, and Generics Don't work!
'''
struct Model {
var content : intel
}
struct intel {
let last : Float
let averages : day
let timestamp : Int64
let bid : Float
let ask : Float
}
struct day {
let day : Float
}
//Intend to change literal value of content at runtime
//Sample Data!
{
"BTCEUR": {
"last": 9477.207190353169,
"averages": {
"day": 8913.97014278919
},
"timestamp": 1561195263,
"bid": 9473.843088382904,
"ask": 9477.676160131046
},
"BTCUSD": {
"last": 10791.221908483476,
"averages": {
"day": 10149.89204777
},
"timestamp": 1561195263,
"bid": 10787.391373795912,
"ask": 10791.755900918146
}
}
'''
Trying to change the name of a property at runtime is the wrong way to solve this problem. A much better solution is to just decode your JSON into a dictionary.
We can create a typealias like this:
typealias BitcoinData = [String: intel]
and decode the data like this:
let decoder = JSONDecoder()
let bitcoinData = try! decoder.decode(BitcoinData.self, from: data)
print(bitcoinData["BTCUSD"]!.averages.day) // 10149.892
Note how we access BTCUSD using the subscript syntax.
In fact, your JSON structure is quite awkward to decode. As you can see, we had to decode to a dictionary here. It would be much better if you could change the JSON structure.
Related
The JSON:
let jsonString = """
{
"groups": [
{
"id": "oruoiru",
"testProp": "rhorir",
"name": "* C-Level",
"description": "C-Level"
},
{
"id": "seese",
"testProp": "seses",
"name": "CDLevel",
"description": "CDLevel"
}
],
"totalCount": 41
}
"""
Type:
struct Group: Codable {
var id: String
var name: String
}
I would like to decode this JSON to only output an array of Group type without having to create boilerplate type like:
struct GroupsResponse: Codable {
var groups: [Group]
var totalCount: Int
}
and use:
let data = jsonString.data(using: .utf8)
let decoded = try! JSONDecoder().decode([Group].self, from: data!)
I tried getting the containers from inside the initialiser of the Group type, but the program already crashes outside at the decoding line with Swift.DecodingError.typeMismatch error
One solution that does work is doing something like:
let topLevel = try! JSONSerialization.jsonObject(with: data) as? [String: Any]
let groupsNode = topLevel?["Groups"] as? [[String: Any]]
let groups = try! JSONSerialization.data(withJSONObject: groupsNode!)
let decoded = try! JSONDecoder().decode([Group].self, from: groups)
but this seems very hacky. Is there an elegant way to handle this?
You cannot avoid the top level response struct using JSONDecoder. There has to be a type for it to work on. And you can't use Dictionary as the top level object (ie [String: [Group]]), since there's a totalCount field that doesn't have an array of Group. All the comments are correct. Just write the little wrapper. It's one line long:
struct GroupsResponse: Codable { var groups: [Group] }
There's no need to decode fields you don't care about.
But you said "for education," and it's all code, so of course you can replace JSONDecoder with something that can do this. You tried to do that with NSJSONSerialization, but that's extremely clunky. You can also just write your own version of JSONDecoder, and then do it like this:
let decoder = RNJSONDecoder()
let response = try decoder.decode(JSON.self, from: json)
let groups = try decoder.decode([Group].self, from: response.groups)
This avoids any re-encoding (RNJSONDecoder can decode directly from a JSON data structure; it doesn't have to convert it back to Data first). It also requires about 2600 lines of incredibly tedious boilerplate code, mostly copy and pasted out of stdlib. Implementing your own Decoder implementation is obnoxious.
If you wanted to get fancier, you could scan the data for the section that corresponds to the property you want, and decode just that part. While implementing the Decoder protocol is very hard, parsing JSON is quite straight-forward. I'm currently doing a bunch of JSON experiments, so I may try writing that and I'll update this if I do.
But the answers are: "just write the tiny, simple, fast, easy to understand response wrapper," or "replace JSONDecoder with something more powerful."
UPDATE:
I went ahead and built the scanner I mentioned, just to show how it could work. It's still a bit rough, but it allows things like:
let scanner = JSONScanner()
let groupJSON = try scanner.extractData(from: Data(jsonString.utf8),
forPath: ["groups", 1])
let group = try JSONDecoder().decode(Group.self, from: groupJSON)
XCTAssertEqual(group.id, "seese")
So you can just extract the part of the data you want to parse, and not worry about parsing the rest.
I'm trying to access the data downloaded from a website which looks like this:
{"quizzes":[
{"id":1, "question": "Can't solve this :("},
{"id":2, "question": "Someone help pls"}]}
The data downloaded looks way more complex, with more values, more keys, and some keys being associated to another dictionary of Key:String, but since I can't even access the most simple fields I thought I would start with this.
I'm using JSONSerialization, so my question is:
If I want to create a variable where i can save the downloaded data... Which would be it's type? I would say [String:[String:Any]], but I'm not sure if "quizzes" represents a key on this specific key, since the data starts with '{' and not with '['.
Instead of using JSONSerialization one could use JSONDecoder.
Example
struct Question: Decodable {
let id: Int
let question: String
}
struct Quiz: Decodable {
let quizzes: [Question]
}
Assuming jsonStr is a string with the JSON in your question:
if let jsonData = jsonStr.data(using: .utf8) {
if let quiz = try? JSONDecoder().decode(Quiz.self, from: jsonData) {
for question in quiz.quizzes {
print("\(question.id) \"\(question.question)\"")
}
}
}
Output
1 "Can't solve this :("
2 "Someone help pls"
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
The new Swift "Decoder" class sounds like a great way to parse JSON data, but all of the examples I've found use a well-known, well-defined 'struct' to do so.
In my case I'm querying an arbitrary website that returns a HUGE JSON string and I only care about a few of the (deeply nested) fields, so I don't want to take all that time to define a 'struct' to get at them.
Is it even possible to do this with "Decoder"? And if so, how does one go about it?
The question seems to be based on a misapprehension about how Decodable works. As a convenience, Decodable is willing to do some automatic code generation behind the scenes so that you can define a struct or nest of structs and just decode the entirety of the JSON. But you are not required to take advantage of that in order to decode JSON.
There is no need to define struct properties for "fields" you don't care about. If a JSON dictionary contains 100 keys and your corresponding struct contains just one property, no problem; that key will be fetched, and no others.
With regard to the "deeply nested" part, it should not take you much time to write simple nested structs that perform the dive to reach the dictionary you really care about. But if you don't want to do even that, you could write an implementation of init(from:) that dives down and fetches out the desired values.
In other words, if you think of Decodable as consisting primarily of your implementation of init(from:), and learn to write the code that it needs, you will see that this JSON can be parsed in a few quick simple lines of code.
As an example, here's a JSON sketch of a deeply nested piece of information with a bunch of extra information at every level that we want to ignore:
{
"ignore": true,
"outer1": {
"ignore": true,
"outer2": {
"ignore": true,
"outer3": {
"name": "matt",
"ignore": true
}
}
}
}
What I'd like to do is define a very simple struct Person that consists solely of the deeply nested name:
struct Person : Decodable {
let name : String
}
I can do that! To do so, I implement Decodable myself, supplying a "hoover" CodingKey adopter struct and an implementation of init(from:), like this (this may look like a lot of work, but it isn't, because the AnyCodingKey implementation is boilerplate, copied and pasted from here, and the init(coder:) implementation is just a few lines of code that were easy to write):
struct Person : Decodable {
let name : String
struct AnyCodingKey : CodingKey {
var stringValue: String
var intValue: Int?
init(_ codingKey: CodingKey) {
self.stringValue = codingKey.stringValue
self.intValue = codingKey.intValue
}
init(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
var con = try! decoder.container(keyedBy: AnyCodingKey.self)
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer1"))
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer2"))
con = try! con.nestedContainer(keyedBy: AnyCodingKey.self, forKey: AnyCodingKey(stringValue:"outer3"))
let name = try! con.decode(String.self, forKey: AnyCodingKey(stringValue:"name"))
self.name = name
}
}
When I want to dive into the JSON and grab the name information, it's trivial:
let person = try! JSONDecoder().decode(Person.self, from: json)
The result is a Person object with name value "matt". Note that I didn't have to add any of the ignore keys and I didn't need to make a nest of structs.
Sure you can achieve this but with both JSonSerialization & Decodable , you have to serialize the json until reach the desired content then decode it ,but instead I recommend to create structs for root keys only then decode , think of it as it's a path from top to bottom don't decode a key that isn't in the path of your desired content
Hi i am new to iOS and I am using Alamofire for network calls. The things were going good and I am facing no trouble in making network calls. But since I have to post my custom object I am having no luck. so here are the things I was doing before
let parameters: Parameters = [
"Phone": phone,
"ApiKey":"x-y-z"
]
this was working fine.
but now I have to post my objects like
let parameters: Parameters = [
"ApiKey": Common.API_KEY,
"cardModel": cardModel,
"clientModel" : clientModel
]
My cardModel and client model are already converted in Json string i am just putting them into dictionary. the converted model looks like these
"cardModel": {
"ExpiryYear": 2018,
"ExpiryMonth": 1,
"CardNumber": "55555",
"CardHolderName": "xyz"
}
so I am putting these serialized models in the dictionary and post this data into request body using Alamofire.
But on server side these Models are null. Any idea how to put custom model in the way I want ? please help
PS I just print out my parameters dictionary and I have examined this output
["ApiKey": "x-y-z",
"\cardModel\": "{
"\ExpiryYear\": 2018,
"\ExpiryMonth\": 1,
"\CardNumber\": "\55555\",
"\CardHolderName\": "\xyz\"
}
]
I put that parameters json printed output in jsonLint and it was wrong format. I just removed the "\" and replaced [] with {} and then it appears to be valid Json
So what I should do now???? please help
Update1:
this is valid json for my endpoint (sending from android)
{
"ApiKey": "XXX-XXX-XXX",
"cardModel": {
"CardHolderName": "Fjj",
"CardNumber": "555",
"CardExpiryMonth": 1,
"CardExpiryYear": 2018
......
}
}
First you need to have a method/computed property that converts your model into a dictionary of type [String: AnyObject] or [String: Any].
Then instead of adding your model into your parameter dictionary add the model dictionary like in below example.
class CardModel: NSObject {
var expiryYear: Int?
var expiryMonth: Int?
var cardNumber: String?
var cardHolderName: String?
var jsonDict: [String: AnyObject] {
var json: [String: AnyObject] = [:]//Initializing an Empty Dictionary
json["expiryYear"] = expiryYear as AnyObject // Inserting expiryYear into Dictionary
json["expiryMonth"] = expiryMonth as AnyObject // Inserting expiryMonth into Dictionary
json["cardNumber"] = cardNumber as AnyObject // Inserting cardNumber into Dictionary
json["cardHolderName"] = cardHolderName as AnyObject // Inserting cardHolderName into Dictionary
return json // returning newly populated dictionary
}
}
func requestToServer(cardModel: CardModel) {
var parameters = [String: AnyObject]()
parameters["ApiKey"] = "dfv12345df234t" as AnyObject
parameters["cardModel"] = cardModel.jsonDict as AnyObject// adding card model dictionary into you paramters dictionary.
//Same logic will be use for your next clientModel
}
I suggest you read up on the Codable protocol, it is a very elegant way to map basic Swift-types to JSON-data. A Playground will help you with the following
import Cocoa
let jsonData = """
{
"ApiKey": "XXX-XXX-XXX",
"cardModel": {
"CardHolderName": "Fjj",
"CardNumber": "555",
"CardExpiryMonth": 1,
"CardExpiryYear": 2018
}
}
""".data(using: .utf8)!
// ... missing here (but added a closing brace for cardModel
struct CardModel: Codable {
let holderName: String
let number: String
let expiryMonth: Int
let expiryYear: Int
enum CodingKeys: String, CodingKey {
case holderName = "CardHolderName"
case number = "CardNumber"
case expiryMonth = "CardExpiryMonth"
case expiryYear = "CardExpiryYear"
}
}
struct ApiCardPayment: Codable {
let apiKey: String
let cardModel: CardModel
enum CodingKeys: String, CodingKey {
case apiKey = "ApiKey"
case cardModel
}
}
do {
let apiPayment = try JSONDecoder().decode(ApiCardPayment.self, from:jsonData)
print(apiPayment)
} catch {
print(error)
}
This is much easier to handle than the [String:AnyObject] casting nightmare you will probably have to interpret otherwise. Besides the error messages of the JSONDecoder have been improving rapidly and it is now rather good at pointing out what is going wrong.
P.S.: Of course there is also JSONEncoder().encode(), but that is just the easy part anyways.
I'm trying to make an app that will select an index at random, something like 0-1000 and then print the key, value, and link of the selected number to three seperate labels on the iPhone simulator.
So from my example, I want to randomly select "0" or "1" and if for instance "1" was chosen; then the key, value, and link information would each be printed to three separate labels on the simulator. The following is what I've been working on in playgrounds. Is there a better way to go about this?
var spDictionary: [String: [String:String]] = [
"0": ["key": "AMZN", "value": "AMAZON", "link": "yahoo"],
"1": ["key": "AAPL", "value": "APPLE", "link": "yahoo2"],
]
And for the random aspect I think it would be something like this but I'm not sure? Sorry for the newbie question.
let randomIndex: Int = Int(arc4random_uniform(UInt32(spDictionary.count)))`
Even for small data structures it's worth it to create a custom class or struct
struct Data {
let key : String
let value : String
let link : String
}
Create an object
let data = Data(key: "AMZN", value: "AMAZON", link: "yahoo")
Get a property
let link = data.link
and you can declare your dictionary
var spDictionary : [String: Data] = ...