Created
February 14, 2019 21:06
-
-
Save rnapier/8c4c70ec2618a7f3d9fb687eed474a4c to your computer and use it in GitHub Desktop.
Custom key decoding strategy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
let json = Data(""" | |
{ | |
"user_name":"Mark", | |
"user_info":{ | |
"b_a1234":"value_1", | |
"c_d5678":"value_2" | |
} | |
} | |
""".utf8) | |
// Makes me sad, but it's private to JSONEncoder.swift | |
// https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift | |
func convertFromSnakeCase(_ stringKey: String) -> String { | |
guard !stringKey.isEmpty else { return stringKey } | |
// Find the first non-underscore character | |
guard let firstNonUnderscore = stringKey.firstIndex(where: { $0 != "_" }) else { | |
// Reached the end without finding an _ | |
return stringKey | |
} | |
// Find the last non-underscore character | |
var lastNonUnderscore = stringKey.index(before: stringKey.endIndex) | |
while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" { | |
stringKey.formIndex(before: &lastNonUnderscore) | |
} | |
let keyRange = firstNonUnderscore...lastNonUnderscore | |
let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore | |
let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex | |
var components = stringKey[keyRange].split(separator: "_") | |
let joinedString : String | |
if components.count == 1 { | |
// No underscores in key, leave the word as is - maybe already camel cased | |
joinedString = String(stringKey[keyRange]) | |
} else { | |
joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined() | |
} | |
// Do a cheap isEmpty check before creating and appending potentially empty strings | |
let result : String | |
if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) { | |
result = joinedString | |
} else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) { | |
// Both leading and trailing underscores | |
result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange]) | |
} else if (!leadingUnderscoreRange.isEmpty) { | |
// Just leading | |
result = String(stringKey[leadingUnderscoreRange]) + joinedString | |
} else { | |
// Just trailing | |
result = joinedString + String(stringKey[trailingUnderscoreRange]) | |
} | |
return result | |
} | |
struct AnyKey: CodingKey { | |
var stringValue: String | |
var intValue: Int? | |
init(stringValue: String) { | |
self.stringValue = stringValue | |
self.intValue = nil | |
} | |
init(intValue: Int) { | |
self.stringValue = String(intValue) | |
self.intValue = intValue | |
} | |
} | |
func convertFromSnakeCase(exceptWithin: [String]) -> ([CodingKey]) -> CodingKey { | |
return { keys in | |
let lastKey = keys.last! | |
let parents = keys.dropLast().compactMap {$0.stringValue} | |
if parents.contains(where: { exceptWithin.contains($0) }) { | |
return lastKey | |
} | |
else { | |
return AnyKey(stringValue: convertFromSnakeCase(lastKey.stringValue)) | |
} | |
} | |
} | |
let decoder = JSONDecoder() | |
decoder.keyDecodingStrategy = .custom(convertFromSnakeCase(exceptWithin: ["userInfo"])) | |
struct User: Decodable { | |
let userName: String | |
let userInfo: [String : String] | |
} | |
let user = try decoder.decode(User.self, from: json) | |
print(user) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment