Skip to content

Instantly share code, notes, and snippets.

@takasek
Last active August 5, 2020 06:08
Show Gist options
  • Save takasek/ae75a48d1da1f83da64dc98b5a840bf2 to your computer and use it in GitHub Desktop.
Save takasek/ae75a48d1da1f83da64dc98b5a840bf2 to your computer and use it in GitHub Desktop.
CodingKeyをJSONのkeyに寄せて書くことができた!
// 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")
@takasek
Copy link
Author

takasek commented Feb 6, 2019

motivation

JSONのkeyがsnake_case、型のプロパティがcamelCaseのとき、
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
で両者の矛盾をある程度埋められる。

が、その場合 CodingKeys の文字列表現が、JSONのkeyでもプロパティ名でもない、中途半端なものになってしまうケースが多々発生する。

struct Hoge: Decodable {
    enum CodingKeys: String, CodingKey {
        // JSONのkey "hoge_url" ではない
        // propertyの"hogeURL" でもない
        // _convertFromSnakeCase("hoge_url")した結果にマッチするものを書かなければならない
        case hogeURL = "hogeUrl"
    }
    let hogeURL: Int
}

これが気持ち悪いので、JSONのkeyに寄せて書けないかを模索した。

メリデメ検討

  • メリット:
    • JSONのkeyをそのまま書けるのでわかりやすい
  • デメリット:
    • CodingKeyへのハックなので、CodingKeyを省略できない
    • .convertFromSnakeCase との併用は不可
      • .convertFromSnakeCase はJSON側のkeyをコンバートするものなので
    • オレオレ実装なので、すごく負債になりうる

結論

やっぱやりたくねーなーと思いました
.convertFromSnakeCase に寄せるほうがマシだと思います

この方向性で標準APIを改善するなら

CodingKeysを省略しつつ、CodingKeysの stringValue_convertToSnakeCase されることを表現できなければいけない
となると、こんな感じになる…?

protocol CodingKeysConvertingStrategyProtocol {}
struct DefaultCodingKeysConvertingStrategy: CodingKeysConvertingStrategyProtocol {}
struct SnakeCaseConvertibleCodingKeysConvertingStrategy: CodingKeysConvertingStrategyProtocol {}

protocol Decodable {
    associatedtype CodingKeysConvertingStrategy: CodingKeysConvertingStrategyProtocol
}
extension Decodable {
    typealias CodingKeysConvertingStrategy = DefaultCodingKeysConvertingStrategy
}

struct Hoge: Decodable {
    typealias CodingKeysConvertingStrategy = SnakeCaseConvertibleCodingKeysConvertingStrategy

    let hogeURL: URL
    let fooBar: Int
    let customString: String
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment