Декодируемый keyDecodingStrategy настраиваемая обработка словарей

У меня есть следующий объект 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 для ручного преобразования регистра змейки.


person Mark    schedule 14.02.2019    source источник
comment
Поскольку .convertFromSnakeCase является свойством декодера, а не ваших Codable типов, я не понимаю, как это возможно.   -  person Joakim Danielson    schedule 14.02.2019
comment
Хм, это прискорбно.   -  person Mark    schedule 14.02.2019


Ответы (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, который тоже должен быть в 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.

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

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

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

И результат:

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

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

person Rob Napier    schedule 14.02.2019
comment
Идеально! Я согласен, что использование ключей кодирования, вероятно, является более простым решением, но хорошо иметь другой рабочий вариант. - person Mark; 14.02.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"
    }
}
person Den Schigrov    schedule 14.02.2019
comment
Спасибо за ответ! Я упоминаю в вопросе, что это мой текущий обходной путь. - person Mark; 14.02.2019