Say you have a struct
for a model of your API response. Let's say it has 50 members. However, 5-7 members are non-standard casing, you could have AUsernAme
or _BTmember
, but the rest are all snake case credit_score
or status_code
.
Rather than writing all members like this:
struct MyStruct {
let aUserName: String
// 50 more...
private enum CodingKeys: String, CodingKey {
case aUserName = "AUsernAme"
// 50 more...
}
}
Is there a way that we can write it like this?
struct MyStruct {
@CodingKey("AUsernAme") let aUserName: String
let creditScore: Int
// 50 more ...
}
CodePudding user response:
The solution which Sweeper provided is a great solution to your problem, but IMO it may display great complexity to your problem and to the next developers who will read this code.
If I were you, I would just stick to writing all the CodingKeys for simplicity. If your worry is writing a lot of lines of cases, you can write all the cases that doesn't need custom keys in one line and just add the keys with unusual/non-standard casing on new lines:
case property1, property2, property3, property4, property5...
case property50 = "_property50"
And since you mentioned that the rest are in snake case, not sure if you know yet, but we have JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase
.
Hope this helps `tol! :)
CodePudding user response:
How about setting a custom keyDecodingStrategy
just before you decode instead?
struct AnyCodingKey: CodingKey, Hashable {
var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
let mapping = [
"AUsernAme": "aUserName",
// other mappings...
]
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
let key = codingPath[0].stringValue
guard let mapped = mapping[key] else { return codingPath.last! }
return AnyCodingKey(stringValue: mapped)
})
This assumes your JSON has a single level flat structure. You can make this into an extension:
extension JSONDecoder.KeyDecodingStrategy {
static func mappingRootKeys(_ dict: [String: String]) -> JSONDecoder.KeyDecodingStrategy {
return .custom { codingPath in
let key = codingPath[0].stringValue
guard let mapped = dict[key] else { return codingPath.last! }
return AnyCodingKey(stringValue: mapped)
}
}
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .mappingRootKeys(mapping)
If your JSON has more levels, you can change the type of the dictionary to [JSONPath: String]
, where JSONPath
is a type that you can create that represents a key in a nested JSON. Then add a bit of code that converts the coding path, which is just an array of coding keys, to JSONPath
. This should not be hard to write on your own.
A simple way is to just use [AnyCodingKey]
as JSONPath
, but there are many other ways too, and I encourage you to experiment and find the one you like the best.
typealias JSONPath = [AnyCodingKey]
extension AnyCodingKey {
init(codingKey: CodingKey) {
if let int = codingKey.intValue {
self.init(intValue: int)
} else {
self.init(stringValue: codingKey.stringValue)
}
}
}
extension JSONDecoder.KeyDecodingStrategy {
static func mappingRootKeys(_ dict: [JSONPath: String]) -> JSONDecoder.KeyDecodingStrategy {
return .custom { codingPath in
guard let mapped = dict[codingPath.map(AnyCodingKey.init(codingKey:))] else { return codingPath.last! }
return AnyCodingKey(stringValue: mapped)
}
}
}
let mapping = [
[AnyCodingKey(stringValue: "AUsernAme")]: "aUserName"
]
It is not possible to use a property wrapper for this. Your property wrapper @CodingKey("AUsernAme") let aUserName: String
will be compiled to something like this (as per here):
private var _aUserName: CodingKey<String> = CodingKey("AUsernAme")
var aUserName: String {
get { _aUserName.wrappedValue }
set { _aUserName.wrappedValue = newValue }
}
There are two main problems with this:
Assuming you don't want to write
init(from:)
for all the 50 properties inMyStruct
, code will be synthesised to decode it, assigning to its_aUserName
property. You only have control over theinit(from:)
initialiser of theCodingKey
property wrapper, and you cannot do anything about howMyStruct
is decoded in there. IfMyStruct
is contained in another struct:struct AnotherStruct: Decodable { let myStruct: MyStruct }
Then you can indeed control the coding keys used to decode
myStruct
by marking it with a property wrapper. You can do whatever you want in the decoding process by implementing the property wrapper'sinit(from:)
, which brings us to the second problem:The coding key you pass to the
CodingKey
property wrapper is passed via an initialiser of the forminit(_ key: String)
. But you control the decoding via the initialiserinit(from decoder: Decoder)
because that is what will be called when the struct is decoded. In other words, there is no way for you to send the key mappings to the property wrapper.