Converting from snake case to camel case using keyDecodingStrategy - ios

I am trying to use Codable to automatically parse incoming JSON into my models. It is working fine, but then I learnt about _keyDecodingStrategy_, and wanted to use this. It is working great and able to lessen my code as I do not have to write CodingKeys enum for my models.
But now the problem is a new variable sent from server. The variable is post_url_110x110.
I thought it will convert to postUrl110x110, but it doesn't. Do help me into its camelCase conversion or suggest if I should avoid automatic conversion in this case.

It will work for you if you rename your data model property from postUrl110x110 to postUrl110X110 with capital X. I know it's not ideal solution but it's worth noting. Check the example below:
struct DataItem: Codable {
var itemId: String
var postUrl110X110: String
}
let json = """
{
"item_id": "abcd",
"post_url_110x110": "https://example.org/image.png"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
print(try! decoder.decode(DataItem.self, from: json))

You can try this tool.
Sample input
Output
If you need further assistance then let me know.

Related

Decode JSON array inside a JSON dictionary without creating Boilerplate Types

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.

Parsing JSON file using Swift

Here is the general structure of a team from our JSON file:
{"Team11":{
"results":{
"leg1":[
{"g":"m","n":"Name1"}
],"leg2":[
{"g":"m","n":"Name2"}
],"leg3":[
{"g":"m","n":"Name3"}
],"leg4":[
{"g":"m","n":"Name4"}
],"leg5":[
{"g":"m","n":"Name5"}
],"leg6":[
{"g":"m","n":"Name6"}
],"leg7":[
{"g":"m","n":"Name7"},{"g":"m","n":"Name8"}
]
},"tn":"TeamName",
"division":"co"
}
}
So far we are able to parse up into results categories leg1, leg2, etc. Accessing the info contained in the bracket arrays has not worked so far.
My current idea on why it is failing is because we are storing the JSON teams incorrectly via String:Any.
My other theory is I just haven't been able to find the correct documentation. Any pointers on where to look or tips would be huge!
Make sure to add What hasn't worked for you and what you have tried? As a New contributor you need to learn how to post questions. Give it a try with my below answer.
Use Codable to parse the JSON like below,
let welcome = try? newJSONDecoder().decode(Welcome.self, from: jsonData)
// Welcome
struct Welcome: Codable {
let team11: Team11
enum CodingKeys: String, CodingKey {
case team11 = "Team11"
}
}
// Team11
struct Team11: Codable {
let results: [String: [Result]]
let tn, division: String
}
// Result
struct Result: Codable {
let g, n: String
}
Note: Your JSON is missing open and ending curly brackets, and I've updated that in your question.
Are you trying to parse the JSON manually? I'm not sure where you're at with your code, but the standard way to parse a JSON string into an object is this:
let jsonData = myJSONString.data(using: .utf8)
There is an issue with your JSON though. You can validate a JSON file on this link.

Can't retrieve the info after downloading this data

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"

How to use Codable protocol with reference types?

Example:
import Foundation
class Player: Codable {
let name: String
init(name: String) {
self.name = name
}
}
class Team: Codable {
let players: [Player]
let capitan: Player
init(players: [Player], capitan: Player) {
self.players = players
self.capitan = capitan
}
}
let player1 = Player(name: "p1")
let player2 = Player(name: "p2")
let team = Team(players: [player1, player2], capitan: player1)
print(team.players[0] === team.capitan) // true
let encoder = JSONEncoder()
let data = try encoder.encode(team)
let decoder = JSONDecoder()
let team2 = try decoder.decode(Team.self, from: data)
print(team2.players[0] === team2.capitan) // false
Output:
true
false
How to use Codable protocol with reference types?
The behavior may change in the future?
https://github.com/apple/swift-evolution/blob/master/proposals/0167-swift-encoders.md
This is fundamental to JSON, not Codable. JSON is an encoding of values. It does not keep track of references; it's not part of the format. If you want to keep track of references, you would have to store that in the JSON yourself, by encoding objectIdentifier. You'd then need to join up things that have the same identifier. But that's beyond the scope of Codable, since the underlying format doesn't have the concept. You'd need to create some other format (perhaps that used JSON as its underlying value format, in the way that NSKeyedArchiver uses property lists as its underlying value format).
JSON isn't an object graph storage format; as I said it's a value encoding. If you want an object graph, that's what NSKeyedArchiver is for. If you want a non-Cocoa solution, you would need to implement a new Encoder and Decoder that implemented the same basic idea as the archivers. That isn't what JSONEncoder is for. But I'd use NSKeyedArchiver if possible; it's designed for this.
Note that SE-0167 says that there is a new encodeCodable method on NSKeyedArchiver that is supposed to let you intermix Codable types with NSCoding types (and allow Swift types to participate in NSKeyedArchiver), but I don't see it in Xcode 9 beta 1.
It seems like the encoder currently does not respect references and instead saves a separate instance of the same object each time it is referenced, which is why === fails. You can change it to == and implement Equatable for your classes which should fix this, or as the comment on the question states, use structs.

Generalized JSON parser for Swift

In our iOS project, we are using SwiftyJSON and ObjectMapper to parse JSON responses and store them in models. However, for the models, we have to manually specify the mapping. eg; if I have a model class called User, which has name and age as properties, then while parsing, I have to specify the following in the User class:
func mapping(map: Map) {
name <- map["Name"]
age <- map["Age"]
}
Doing the same for all models is tedious and time consuming. Isn't there an approach to generalize the parsing? Like I pass any JSON to a function and specify the model, and the function should return me the model object with the parsed values, if they're available. I don't want to write a separate mapping for each model.
I think you should take a look at EVReflection, a Swift 3 library which does what you are looking for.
Update:
Swift 4 (now in Beta) seems to be bringing some changes which are relevant to this question.
struct MyStruct: Codable {
var str: String
var num: Int
}
let myStruct = MyStruct(str: "test", num:5)
let encoder = JSONEncoder()
let jsonData = try encoder.encode(myStruct)
let json = String(data: jsonData, encoding: .utf8)
print(json) // prints {"str": "test", "num": 5}
// Also decoding
let decoder = JSONDecoder()
let decoded = try decoder.decode(MyStruct.self, from: jsonData)
// decoded is a structure of type MyStruct
print(decoded.str) // test

Resources