Last active
August 5, 2020 06:08
-
-
Save takasek/ae75a48d1da1f83da64dc98b5a840bf2 to your computer and use it in GitHub Desktop.
CodingKeyをJSONのkeyに寄せて書くことができた!
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
// https://github.com/apple/swift/blob/b0f5815d2b003df628b1bcfe94681fec489c9492/stdlib/public/Darwin/Foundation/JSONEncoder.swift#L153 | |
func _convertToSnakeCase(_ stringKey: String) -> String { | |
guard !stringKey.isEmpty else { return stringKey } | |
var words : [Range<String.Index>] = [] | |
// The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase | |
// | |
// myProperty -> my_property | |
// myURLProperty -> my_url_property | |
// | |
// We assume, per Swift naming conventions, that the first character of the key is lowercase. | |
var wordStart = stringKey.startIndex | |
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex | |
// Find next uppercase character | |
while let upperCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.uppercaseLetters, options: [], range: searchRange) { | |
let untilUpperCase = wordStart..<upperCaseRange.lowerBound | |
words.append(untilUpperCase) | |
// Find next lowercase character | |
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound | |
guard let lowerCaseRange = stringKey.rangeOfCharacter(from: CharacterSet.lowercaseLetters, options: [], range: searchRange) else { | |
// There are no more lower case letters. Just end here. | |
wordStart = searchRange.lowerBound | |
break | |
} | |
// Is the next lowercase letter more than 1 after the uppercase? If so, we encountered a group of uppercase letters that we should treat as its own word | |
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound) | |
if lowerCaseRange.lowerBound == nextCharacterAfterCapital { | |
// The next character after capital is a lower case character and therefore not a word boundary. | |
// Continue searching for the next upper case for the boundary. | |
wordStart = upperCaseRange.lowerBound | |
} else { | |
// There was a range of >1 capital letters. Turn those into a word, stopping at the capital before the lower case character. | |
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) | |
words.append(upperCaseRange.lowerBound..<beforeLowerIndex) | |
// Next word starts at the capital before the lowercase we just found | |
wordStart = beforeLowerIndex | |
} | |
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound | |
} | |
words.append(wordStart..<searchRange.upperBound) | |
let result = words.map({ (range) in | |
return stringKey[range].lowercased() | |
}).joined(separator: "_") | |
return result | |
} | |
protocol CodingKeyToSnake: CodingKey, RawRepresentable where Self.RawValue == String {} | |
extension CodingKeyToSnake { | |
var stringValue: String { | |
return _convertToSnakeCase(rawValue) | |
} | |
} | |
let json = """ | |
{ | |
"foo_bar": 1, | |
"hoge_url": "https://google.com", | |
"custom_suruyo": "zzz" | |
} | |
""" | |
struct Hoge: Decodable { | |
enum CodingKeys: String, CodingKeyToSnake { | |
case fooBar | |
case hogeURL | |
case customString = "custom_suruyo" | |
} | |
let hogeURL: URL | |
let fooBar: Int | |
let customString: String | |
} | |
let jsonData = json.data(using: .utf8)! | |
let obj = try! JSONDecoder().decode(Hoge.self, from: jsonData) | |
print(obj) | |
// Hoge(hogeURL: https://google.com, fooBar: 1, customString: "zzz") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
motivation
JSONのkeyがsnake_case、型のプロパティがcamelCaseのとき、
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
で両者の矛盾をある程度埋められる。
が、その場合
CodingKeys
の文字列表現が、JSONのkeyでもプロパティ名でもない、中途半端なものになってしまうケースが多々発生する。これが気持ち悪いので、JSONのkeyに寄せて書けないかを模索した。
メリデメ検討
.convertFromSnakeCase
との併用は不可.convertFromSnakeCase
はJSON側のkeyをコンバートするものなので結論
やっぱやりたくねーなーと思いました
.convertFromSnakeCase
に寄せるほうがマシだと思いますこの方向性で標準APIを改善するなら
CodingKeysを省略しつつ、CodingKeysの
stringValue
が_convertToSnakeCase
されることを表現できなければいけないとなると、こんな感じになる…?