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
Related
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
}
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
}
}
I have a service that returns this JSON structure "actual-price": {,
I want to know if it is possible to create a variable like JS with Codable on swift.
PS: I can't change the JSON since the service is not mine
You will need to use a CodingKeys enumeration to map the jSON properties to valid Swift properties. Note that once you introduce a CodingKeys enumeration it must contain all of the properties you wish to map, not just the properties where you want to change the name.
Something like
struct MyStruct: Codable {
var actualPrice: Double
var quantity: Int
enum CodingKeys: String, CodingKey {
case actualPrice = "actual-price"
case quantity = "quantity"
}
}
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
}
}
How to get the raw value from a enum passing the key value? Must work for any enum types working like an extension of enum types.
Any mirror reflection, mappable or RawRepresentable solutions are welcome.
I would like something like this:
enum Test : String {
case One = "1"
}
Test.rawValueFromKey(keyValue: "One") // Must return "1"
// I don't want the solution below, I must get the rawValue through the key name as String.
Test.One.rawValue
I do need to get the rawValue passing the name of the key as a String. Otherwise, I will need to make a switch or many if/else conditions. I have a big Enum and I don't want to check the string passed in a switch for example and pass Test.One.rawValue. I would like to get the rawValue directly through the key as String, just like in a dictionary.
I also don't want the code below, because I have a Enum of 40 items and I think it is ugly to make a switch of 40 items.
switch keyValue {
case "One":
return Test.One.rawValue
break
}
I want something like this:
func rawValueFromKey (keyValue: String) -> Test {
// code here to get the rawValue through the keyValue as String
// return the proper Test Enum
}
I tried some possible solutions using Mirror reflection and enum iterations to find the rawValue through the keyValue but didn't work.
Please give the solution in both Swift 2 and 3.
Thanks
As far as I know, you can't reflect on types so I think you will be able to achieve this only with CustomReflectable or maybe using localized strings for mapping the values/keys. Invariably you'll have to map somehow.
Why not something like this? Sure you are just comparing Strings so it's potentially unsafe (if your enum would conform to CustomStringConvertible, etc), but you can't really avoid that.
I think CaseIterable is available only from Swift 4 though...
protocol KeyRepresentable: CaseIterable {
}
extension KeyRepresentable {
static func fromKey(key: String) -> Self? {
return Self
.allCases
.first { "\($0)" == key }
}
}
Usage:
enum Option: Int, KeyRepresentable {
case a = 1, b = 2
}
print(Option.fromKey(key: "a")?.rawValue)