Декодируемая пользовательская обработка keyDecodingStrategy для словарей - PullRequest
0 голосов
/ 14 февраля 2019

У меня есть следующий объект 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"
}

IМы рассмотрели использование пользовательского keyDecodingStrategy (но недостаточно информации для обработки словарей по-разному), а также пользовательский инициализатор для моей структуры Decodable (похоже, что ключи уже были преобразованы к этому моменту).

Как правильно обращаться с этим (создавая исключение для преобразования ключей только для словарей)?

Примечание. Я бы предпочел сохранить стратегию преобразования регистра в виде змеи, поскольку в моих реальных объектах JSON много свойств в случае со змеей.Мой текущий обходной путь - использовать перечисление CodingKeys для ручного преобразования регистра змей.

Ответы [ 2 ]

0 голосов
/ 14 февраля 2019

В качестве альтернативы вы можете использовать CodingKeys, чтобы у вас было больше контроля и вы могли указать имя для каждого поля.Тогда вам не нужно устанавливать keyDecodingStrategy

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

    enum CodingKeys: String, CodingKey {
        case userName = "user_name"
        case userInfo = "user_info"
    }
}
0 голосов
/ 14 февраля 2019

Да ... но это немного сложно, и, в конце концов, может быть более надежным просто добавить 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, который также должен быть в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 .

Наконец, это все шаблонный мусор.Теперь мы можем понять суть этого.Там нет никакого способа сказать «кроме словари» напрямую.Ключи Coding интерпретируются независимо от любого фактического 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))!
        }
    }
}

При этом нам просто нужна специальная стратегия декодирования ключей (обратите внимание, что здесь используется версионная версия userInfo, поскольку путь CodingKey идет после преобразованияприменяется):

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

И результат:

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

Я не могу обещать, что это стоит того, чтобы просто добавить CodingKeys, но это полезный инструмент для панели инструментов.

...