Что мешает моему преобразованию из String в Int при декодировании с использованием Swift 4 Codable?

Я получаю следующий JSON из сетевого запроса:

{
    "uvIndex": 5
}

Я использую следующий тип для декодирования результата преобразования строки в данные:

internal final class DataDecoder<T: Decodable> {

    internal final class func decode(_ data: Data) -> T? {
        return try? JSONDecoder().decode(T.self, from: data)
    }

}

Ниже представлена ​​модель, в которую должны быть преобразованы данные:

internal struct CurrentWeatherReport: Codable {

    // MARK: Properties

    internal var uvIndex: Int?

    // MARK: CodingKeys

    private enum CodingKeys: String, CodingKey {
        case uvIndex
    }

    // MARK: Initializers

    internal init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let uvIndexInteger = try container.decodeIfPresent(Int.self, forKey: .uvIndex) {
            uvIndex = uvIndexInteger
        } else if let uvIndexString = try container.decodeIfPresent(String.self, forKey: .uvIndex) {
            uvIndex = Int(uvIndexString)
        }
    }

}

Ниже приведены тесты, используемые для проверки декодирования:

internal final class CurrentWeatherReportTests: XCTestCase {

    internal final func test_CurrentWeather_ReturnsExpected_UVIndex_FromIntegerValue() {
        let string =
        """
        {
            "uvIndex": 5
        }
        """
        let data = DataUtility.data(from: string)
        let currentWeatherReport = DataDecoder<CurrentWeatherReport>.decode(data)
        XCTAssertEqual(currentWeatherReport?.uvIndex, 5)
    }

    internal final func test_CurrentWeather_ReturnsExpected_UVIndex_FromStringValue() {
        let string =
        """
        {
            "uvIndex": "5"
        }
        """
        let data = DataUtility.data(from: string)
        let currentWeatherReport = DataDecoder<CurrentWeatherReport>.decode(data)
        XCTAssertEqual(currentWeatherReport?.uvIndex, 5)
    }

}

Разница между тестами заключается в значении uvIndex в JSON; один представляет собой строку, а другой - целое число. Я ожидаю целое число, но я хочу обрабатывать любые случаи, когда значение может вернуться в виде строки, а не числа, поскольку это обычная практика для нескольких API, с которыми я работаю. Однако мой второй тест продолжает давать сбой со следующим сообщением: XCTAssertEqual failed: ("nil") is not equal to ("Optional(5)") -

Я делаю что-то неправильно в отношении протокола Codable, который вызывает этот сбой? Если нет, то почему мой второй тест с этим, казалось бы, простым приведением не проходит?

Скриншот неудачного модульного теста в Xcode


person Nick Kohrn    schedule 30.01.2018    source источник


Ответы (2)


Проблема, которую вы видите, связана с комбинацией того, как ваш init(with: Decoder) написан, и как DataDecoder тип декодирует свой аргумент типа. Поскольку тест не пройден с nil != Optional(5), то либо uvIndex, либо currentWeatherReport являются nil в currentWeatherReport?.uvIndex.

Давайте посмотрим, как uvIndex может быть nil. Поскольку это Int?, оно получает значение по умолчанию nil, если оно не инициализировано иным образом, так что это хорошее место для начала поиска. Как ему может быть присвоено значение по умолчанию?

internal init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let uvIndexInteger = try container.decodeIfPresent(Int.self, forKey: .uvIndex) {
        // Clearly assigned to here
        uvIndex = uvIndexInteger
    } else if let uvIndexString = try container.decodeIfPresent(String.self, forKey: .uvIndex) {
        // Clearly assigned to here
        uvIndex = Int(uvIndexString)
    }

    // Hmm, what happens if neither condition is true?
}

Хм. Таким образом, если при декодировании как Int, так и String произойдет сбой (поскольку значение отсутствует), вы получите nil. Но очевидно, что это не всегда так, поскольку первый тест проходит (и значение действительно есть).

Итак, переходим к следующему режиму отказа: если Int явно декодируется, почему String не декодируется должным образом? Что ж, когда uvIndex является String, следующий вызов декодирования все еще выполняется:

try container.decodeIfPresent(Int.self, forKey: .uvIndex)

Этот вызов возвращает nil только в том случае, если значение отсутствует (т. е. для данного ключа нет значения или значение явно равно null); если значение присутствует, но не является Int, вызов будет throw.

Поскольку возникающая ошибка не перехватывается и явно не обрабатывается, она распространяется немедленно, никогда не вызывая try container.decodeIfPresent(String.self, forKey: .uvIndex). Вместо этого ошибка всплывает до того места, где декодируется CurrentWeatherReport:

internal final class func decode(_ data: Data) -> T? {
    return try? JSONDecoder().decode(T.self, from: data)
}

Поскольку этот код try?s, ошибка проглатывается, возвращая nil. Этот nil переходит к исходному вызову currentWeatherReport?.uvIndex, который в конечном итоге становится nil не потому, что uvIndex отсутствует, а потому, что весь отчет не удалось декодировать.

Скорее всего, реализация init(with: Decoder), которая соответствует вашим потребностям, больше похожа на следующую:

internal init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    // try? container.decode(...) returns nil if the value was the wrong type or was missing.
    // You can also opt to try container.decode(...) and catch the error.
    if let uvIndexInteger = try? container.decode(Int.self, forKey: .uvIndex) {
        uvIndex = uvIndexInteger
    } else if let uvIndexString = try? container.decode(String.self, forKey: .uvIndex) {
        uvIndex = Int(uvIndexString)
    } else {
        // Not strictly necessary, but might be clearer.
        uvIndex = nil
    }
}
person Itai Ferber    schedule 30.01.2018
comment
Идеальный. Ваша реализация сработала, как и ожидалось, и ваше объяснение было хорошо понято. - person Nick Kohrn; 30.01.2018
comment
@NickKohrn Отлично, рад, что помог! - person Itai Ferber; 30.01.2018

Вы можете попробовать SafeDecoder.

import SafeDecoder

internal struct CurrentWeatherReport: Codable {

  internal var uvIndex: Int?

}

Затем просто расшифруйте как обычно.

person canius    schedule 15.01.2020