字典的可解码keyDecodingStrategy自定义处理

时间:2019-02-14 19:01:34

标签: json swift codable decodable jsondecoder

我有以下JSON对象:

{
  "user_name":"Mark",
  "user_info":{
    "b_a1234":"value_1",
    "c_d5678":"value_2"
  }
}

我已经这样设置了JSONDecoder

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

我的Decodable对象看起来像这样:

struct User: Decodable {
    let userName: String
    let userInfo: [String : String]
}

我面临的问题是.convertFromSnakeCase策略正在应用于字典的键,我希望这种情况不会发生。

// Expected Decoded userInfo
{
  "b_a1234":"value_1",
  "c_d5678":"value_2"
}

// Actual Decoded userInfo
{
  "bA1234":"value_1",
  "cD5678":"value_2"
}

我一直在研究使用自定义keyDecodingStrategy(但是没有足够的信息来不同地处理字典),以及我的Decodable结构的自定义初始化程序(似乎键已经被使用了)到此为止转换)。

处理此问题的正确方法是什么(仅为词典创建密钥转换例外)?

注意:我宁愿保留蛇格转换策略,因为我的实际JSON对象在蛇格中具有很多属性。我当前的解决方法是使用CodingKeys枚举手动进行蛇格转换。

2 个答案:

答案 0 :(得分:2)

是的...但是,这有点棘手,最后,仅添加CodingKeys可能会更健壮。但这是可能的,并且对自定义密钥解码策略进行了不错的介绍。

首先,我们需要一个函数来进行蛇格转换。我真的很希望在stdlib中公开它,但事实并非如此,而且我不知道有什么方法可以直接复制代码而“到达那里”。因此,这是直接基于JSONEncoder.swift的代码。 (我什至不愿意将其复制到答案中,但是否则您将无法复制其余部分。)

// 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
}

我们还想要一把CodingKey Swiss-Army小刀,它也应该在stdlib中,但不是:

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
    }
}

这仅使您可以将任何字符串转换为CodingKey。它来自JSONDecoder docs

最后,这就是样板垃圾。现在我们可以深入了解它了。无法直接说“在字典中除外”。 CodingKeys的解释与任何实际的Decodable无关。因此,您想要的是一个函数,该函数说:“除非有这样的嵌套在内部的密钥,否则应使用蛇形保护套”。这是一个返回该函数的函数:

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))!
        }
    }
}

有了这一点,我们只需要一个自定义密钥解码策略(注意,因为使用CodingKey路径是在应用转换之后,所以它使用驼峰形式的“ userInfo”):

decoder.keyDecodingStrategy = .custom(convertFromSnakeCase(exceptWithin: ["userInfo"]))

结果:

User(userName: "Mark", userInfo: ["b_a1234": "value_1", "c_d5678": "value_2"])

我不能保证与仅添加CodingKeys相比,这样做值得付出麻烦,但这对于工具箱来说是一个有用的工具。

答案 1 :(得分:0)

或者,您可以使用CodingKeys,这样您就可以控制更多并可以为每个字段指定名称。然后,您不必设置keyDecodingStrategy

struct User: Decodable {
    let userName: String
    let userInfo: [String : String]

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
        case userInfo = "user_info"
    }
}