The `convertFromSnakeCase` strategy doesn't work with custom `CodingKeys` in Swift - ios

I try to use Swift 4.1's new feature to convert snake-case to camelCase during JSON decoding.
Here is the example:
struct StudentInfo: Decodable {
internal let studentID: String
internal let name: String
internal let testScore: String
private enum CodingKeys: String, CodingKey {
case studentID = "student_id"
case name
case testScore
}
}
let jsonString = """
{"student_id":"123","name":"Apple Bay Street","test_score":"94608"}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(StudentInfo.self, from: Data(jsonString.utf8))
print(decoded)
} catch {
print(error)
}
I need provide custom CodingKeys since the convertFromSnakeCase strategy can't infer capitalization for acronyms or initialisms (such as studentID) but I expect the convertFromSnakeCase strategy will still work for testScore. However, the decoder throws error ("No value associated with key CodingKeys") and it seems that I can't use convertFromSnakeCase strategy and custom CodingKeys at the same time. Am I missing something?

The key strategies for JSONDecoder (and JSONEncoder) are applied to all keys in the payload – including those that you provide a custom coding key for. When decoding, the JSON key will first be mapped using the given key strategy, and then the decoder will consult the CodingKeys for the given type being decoded.
In your case, the student_id key in your JSON will be mapped to studentId by .convertFromSnakeCase. The exact algorithm for the transformation is given in the documentation:
Capitalize each word that follows an underscore.
Remove all underscores that aren't at the very start or end of the string.
Combine the words into a single string.
The following examples show the result of applying this strategy:
fee_fi_fo_fum
Converts to: feeFiFoFum
feeFiFoFum
Converts to: feeFiFoFum
base_uri
Converts to: baseUri
You therefore need to update your CodingKeys to match this:
internal struct StudentInfo: Decodable, Equatable {
internal let studentID: String
internal let name: String
internal let testScore: String
private enum CodingKeys: String, CodingKey {
case studentID = "studentId"
case name
case testScore
}
}

Related

Why does defining CodingKeys for SOME properties require an initializer?

Compiles fine with no problems:
class User: Codable {
let name: String
let email: String
}
However, if we have a property not represented by a CodingKey, it demands an initializer:
class User: Codable { // Class 'User' has no initializers
let name: String
let email: String
private enum CodingKeys: String, CodingKey {
case name = "username"
}
}
Why does it decide that a lack of synthesized initializers are the problem, instead of having a warning or error requiring a CodingKey? Why does this break conformance to Decodable?
Edit: Since some people seem to be confused, I'm not asking how to resolve the error. That is obvious. I'm asking what is happening in the Codable protocol when you don't specify a CodingKey for a required property.
A class with at least one property that doesn't have a default value needs an initializer in order to initialize that property.
class User {
let name: String
let email: String
}
Class 'User' has no initializers
When you conform to Codable, or more specifically Decodable as you well noted, the class gets a synthesized initializer, namely User.init(from: Decoder), which fixes above issue. This synthesized initializer will use coding keys named after the properties.
class User: Decodable { ... }
As soon as you define an enum within User called CodingKeys (the exact name is important), the synthesis of the Decodable conformance will require that there is a key for each property. In other words, as soon as one key deviates from the property name, you have to define a coding key for each property.
The following does that and compiles:
class User: Decodable {
let name: String
let email: String
private enum CodingKeys: String, CodingKey {
case name = "username"
case email
}
}
If not specifying the email key, the compiler will not know how to set that property, only the developer would know how. Hence, the compiler will not synthesize it for you and you have to.
There might be a natural default as shown here:
class User: Decodable {
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case name = "username"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
// Default email:
email = "\(name)#example.com"
}
}
Xcode is throwing the first in a line of errors caused by the second example. If you added an initializer it would next probably tell you about something related to your lack of codingkey for the name property. However, when you don't define CodingKeys, it does is able to create CodingKeys as well as the default initializer for you.
Even though you're not going to need a CodingKey for email, you still have to declare its case, without any value as in:
private enum CodingKeys: String, CodingKey {
case name = "username"
case email
}

"No value associated with key CodingKeys" error always thrown [duplicate]

I try to use Swift 4.1's new feature to convert snake-case to camelCase during JSON decoding.
Here is the example:
struct StudentInfo: Decodable {
internal let studentID: String
internal let name: String
internal let testScore: String
private enum CodingKeys: String, CodingKey {
case studentID = "student_id"
case name
case testScore
}
}
let jsonString = """
{"student_id":"123","name":"Apple Bay Street","test_score":"94608"}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(StudentInfo.self, from: Data(jsonString.utf8))
print(decoded)
} catch {
print(error)
}
I need provide custom CodingKeys since the convertFromSnakeCase strategy can't infer capitalization for acronyms or initialisms (such as studentID) but I expect the convertFromSnakeCase strategy will still work for testScore. However, the decoder throws error ("No value associated with key CodingKeys") and it seems that I can't use convertFromSnakeCase strategy and custom CodingKeys at the same time. Am I missing something?
The key strategies for JSONDecoder (and JSONEncoder) are applied to all keys in the payload – including those that you provide a custom coding key for. When decoding, the JSON key will first be mapped using the given key strategy, and then the decoder will consult the CodingKeys for the given type being decoded.
In your case, the student_id key in your JSON will be mapped to studentId by .convertFromSnakeCase. The exact algorithm for the transformation is given in the documentation:
Capitalize each word that follows an underscore.
Remove all underscores that aren't at the very start or end of the string.
Combine the words into a single string.
The following examples show the result of applying this strategy:
fee_fi_fo_fum
Converts to: feeFiFoFum
feeFiFoFum
Converts to: feeFiFoFum
base_uri
Converts to: baseUri
You therefore need to update your CodingKeys to match this:
internal struct StudentInfo: Decodable, Equatable {
internal let studentID: String
internal let name: String
internal let testScore: String
private enum CodingKeys: String, CodingKey {
case studentID = "studentId"
case name
case testScore
}
}

what is the best way to save json data in swift model

I previously made a post about how to save json data in a model here .
Good now I am developing a project on iOS with swift, and I face the following problem, it happens that sometimes the database administrators change the names of the columns constantly, I only consume services with the help of Alamofire, to save data in models, use camal case and snake case, At the moment everything is fine but I would like to know what is the best way to save json data in a swift model, in my experience with Android I used retrofit with #Serializename and it worked great because if the json attribute of the service it was modified I only had to update a line of code and my variable could be kept the same, this helped me maintain a better order and it made it scalable.
In some cases the json comes to me.
{
"price": "385.000000",
"nameusser": null,
"favorite": 43,
"short_nameProduct": "Génifique Repair Sc",
"description_product": "Génifique repair sc es la crema de noche antiedad de lancôme. Despiértese con una piel fresca y rejuvenecida con nuestra crema de noche.",
"alt": null,
"photo": "https://url/020021000112-1.png"
}
in swift it would generate my model in the following way.
struct Product : Codable {
let price : String?
let nameusser : String?
let favorite : Int
let shortNameProduct : [Product]
let description : [Galery]
let alt : Product
let success : Bool
}
The problem here is that my variables must fit the json I get to use the JSONDecoder() and the convertFromSnakeCase, I can not define them myself.
while in java android I just have to do it like that.
#SerializedName("price")
private String price;
#SerializedName("nameusser")
private String name;
#SerializedName("favorite")
private Int favorite;
#SerializedName("short_nameProduct")
private String shortName;
#SerializedName("description_product")
private String descriptionProduct;
#SerializedName("altitude")
private String altitude;
#SerializedName("photo")
private String photo;
I just have to create the get and set and I would be ready to use the model.
I need to know how to do in swift the same, maybe a library that helps me store data json in the same way that I do in android.
Any comment would be very appreciated.
The best way is to use the Coding Keys:
struct Product : Codable {
let price : String?
let nameusser : String?
let favorite : Int
let shortNameProduct : [Product]
let description : [Galery]
let alt : Product
let success : Bool
enum CodingKeys: String, CodingKey {
case price = "price"
case nameusser = "nameusser"
case favorite = "favorite"
case shortNameProduct = "short_nameProduct"
case description = "description_product"
case alt = "alt"
case success = "success"
}
}
The name of the enum case has to match the property name on the struct. This way you can define whatever keys you want without having to write any custom encoding or decoding code.
Feel free to use my gist here:
Storage
You can use it like this:
let fileName = "Product.json"
extension Product {
func store() {
Storage.store(self, to: .documents, as: fileName)
}
static func retrieve() -> Product? {
guard let product = Storage.retrieve(fileName, from: .documents, as: Product.self) else {
return nil
}
return product
}
}
With Alamofire you will get a key-value, an Dictionary<String,Any> or [String:Any]
So you can do the following with your dictionary:
var myProduct : Product?
if let price = myDictionary["price"] as? String{
myProduct.price = price
}
With that example you can create a method to mapping the entire JSON into your struct. Maybe if you want to make it more scalable, you can create an enum with String raw values and use it as the key for the dictionary, some like:
enum productProperty : String{
case Price = "price"
}
var myProduct : Product?
if let price = myDictionary[productProperty.Price] as? String{
myProduct.price = price
}
And maybe create a more complex class to iterate trough the dictionary and check the key using the enum, but that implementation depends of your own skills.
Edit1:
To use't with alamofire, you need to get the jsonResponse of the request, some like that:
.request(yourURL, method: .get, parameters: yourParameter).responseJSON(options: .allowFragments , completionHandler: { response in
if let object = response.result.value as? Dictionary<String,Any> {
yourMethodToSave(object)
}
})
and inside yourMethodToSave(_ object: Dictionary<String,Any>) you need to put the logic above.
Ps: The #sendtobo answer have the enum example that i tell that you can use to a more scalable mapping for your object

How to use nested json with structs in swift

I have an API and I need to call, to get a list of holidays with some additional info along with it. The link of my API - http://mahindralylf.com/apiv1/getholidays
The structure I created using the website app.quicktype.io
struct Holiday: Codable {
let responseCode, responseMsg: String
let holidayCount: Int
let holidays: [HolidayElement]
enum CodingKeys: String, CodingKey {
case responseCode = "response_code"
case responseMsg = "response_msg"
case holidayCount = "holiday_count"
case holidays
}
}
struct HolidayElement: Codable {
let month: String
let image: String
let details: [Detail]
}
struct Detail: Codable {
let title, date, day: String
let color: Color
}
enum Color: String, Codable {
case b297Fe = "#B297FE"
case e73838 = "#E73838"
case the0D8464 = "#0D8464"
}
I can get to the "Holiday" object, print it, display my tableViewCells with a colour for the "holidayCount". What I want to do is, without using the normal json parsing and making my own arrays and dicts, to access the "Detail" for each "holidays".
tl;dr - I need to know how to access Detail for the holidays element
Thanks!!
Your data's coming back with an array of HolidayElements and each HolidayElement has an array of Details.
So for each HolidayElement, you'd want to get access to the details array. You'd do that like so:
let jsonResponse = try JSONDecoder().decode(Holiday.self, from: responseData)
print(jsonResponse.holidays[0].details)
Here's a repo to play around with.
Additionally, your coding keys are just converting from snake_case, so you don't really need them for that endpoint. Instead, you can just tell the decoder to convertFromSnakeCase
You can ditch the coding keys in this case and just decode as follows:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let jsonResponse = try decoder.decode(Holiday.self, from: responseData)
print(jsonResponse.holidays[0].details)

Get specific CodingKey for KeyPath

Consider the following object:
struct User: Codable {
let id: Int
let email: String
let name: String
}
Is it posible to get a specific CodingKey for a given KeyPath?
let key = \User.name.codingKey // would be equal to string: "name"
Using Swift 4, I don't think you can automatically retrieve a CodingKey from the corresponding KeyPath object but you can always hack your way around it ;)
For instance, in the same User type Swift source file, add the following extension:
fileprivate extension User {
static func codingKey(for keyPath: PartialKeyPath<User>) -> CodingKey {
switch keyPath {
case \User.id: return CodingKeys.id
case \User.email: return CodingKeys.email
case \User.name: return CodingKeys.name
default: fatalError("Unexpected User key path: \(keyPath)")
}
}
}
then implement the desired codingKey API in the constrained KeyPath superclass:
extension PartialKeyPath where Root == User {
var codingKey: CodingKey {
return User.codingKey(for: self)
}
}
Finally, the usage follows closely your code:
let name: CodingKey = (\User.name).codingKey
print("\(name)") // prints "name"
This may be a somewhat tedious and error prone solution but, if you only need this capability for handful of types, it's perfectly doable in my opinion ;)
Caveats. This hack, of course, won't work for externally defined types given CodingKeys enum private visibility. (For instance, for all Codable types defined by the Swift Standard Library.)
I don't think you can convert property directly to the string but you can achieve similar thing using reflection, but you have to create an instance of the struct:
let user = User(id: 1, email: "sample#email.com", name: "just_name")
Mirror(reflecting: user).children.first?.label

Resources