Swift 4 JSON Decodable - самый простой способ декодировать изменение типа

Протокол Codable в Swift 4 обеспечивает отличный уровень скрытых дат и стратегий преобразования данных.

Учитывая JSON:

{
    "name": "Bob",
    "age": 25,
    "tax_rate": "4.25"
}

Я хочу превратить его в следующую структуру

struct ExampleJson: Decodable {
    var name: String
    var age: Int
    var taxRate: Float

    enum CodingKeys: String, CodingKey {
       case name, age 
       case taxRate = "tax_rate"
    }
}

Стратегия декодирования даты может преобразовывать дату на основе String в дату.

Есть ли что-то, что делает это с Float на основе String

В противном случае я застрял в использовании CodingKey для ввода String и использования вычислительного метода get:

    enum CodingKeys: String, CodingKey {
       case name, age 
       case sTaxRate = "tax_rate"
    }
    var sTaxRate: String
    var taxRate: Float { return Float(sTaxRate) ?? 0.0 }

Такого рода нити требуют большего ухода, чем мне кажется.

Это самый простой способ или есть что-то похожее на DateDecodingStrategy для других преобразований типов?

Обновление: я должен отметить: я также пошел путем переопределения

init(from decoder:Decoder)

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


person Dru Freeman    schedule 16.06.2017    source источник
comment
Спасибо @Rob, я исправил вопрос с этой оплошностью.   -  person Dru Freeman    schedule 16.06.2017
comment
Я столкнулся с той же проблемой и обнаружил! Swift bug. Обертывание чисел в виде строк в JSON - это обычное дело, и я надеюсь, что команда Swift справится с этим случаем.   -  person chrismanderson    schedule 19.06.2017
comment
И похоже, что команда Swift изучает эту проблему. Скрещенные пальцы!   -  person chrismanderson    schedule 19.06.2017
comment
См. мой ответ, в котором показано до 3 различных способов решения вашей проблемы.   -  person Imanou Petit    schedule 17.11.2017


Ответы (8)


К сожалению, я не верю, что такая опция существует в текущем JSONDecoder API. Существует только вариант для преобразования исключительных плавающих- значения точек в строковое представление и обратно.

Другое возможное решение для ручного декодирования - определить Codable тип оболочки для любого LosslessStringConvertible, который может кодировать и декодировать из своего String представления:

struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {

    var decoded: Decoded

    init(_ decoded: Decoded) {
        self.decoded = decoded
    }

    init(from decoder: Decoder) throws {

        let container = try decoder.singleValueContainer()
        let decodedString = try container.decode(String.self)

        guard let decoded = Decoded(decodedString) else {
            throw DecodingError.dataCorruptedError(
                in: container, debugDescription: """
                The string \(decodedString) is not representable as a \(Decoded.self)
                """
            )
        }

        self.decoded = decoded
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(decoded.description)
    }
}

Тогда вы можете просто иметь свойство этого типа и использовать автоматически сгенерированное соответствие Codable:

struct Example : Codable {

    var name: String
    var age: Int
    var taxRate: StringCodableMap<Float>

    private enum CodingKeys: String, CodingKey {
        case name, age
        case taxRate = "tax_rate"
    }
}

Хотя, к сожалению, теперь вам нужно говорить о taxRate.decoded, чтобы взаимодействовать со значением Float.

Однако вы всегда можете определить простое вычисляемое свойство пересылки, чтобы облегчить это:

struct Example : Codable {

    var name: String
    var age: Int

    private var _taxRate: StringCodableMap<Float>

    var taxRate: Float {
        get { return _taxRate.decoded }
        set { _taxRate.decoded = newValue }
    }

    private enum CodingKeys: String, CodingKey {
        case name, age
        case _taxRate = "tax_rate"
    }
}

Хотя это все еще не так гладко, как должно быть - надеюсь, более поздняя версия JSONDecoder API будет включать больше настраиваемых параметров декодирования или же будет иметь возможность выражать преобразование типов в самом Codable API.

Однако одним из преимуществ создания типа оболочки является то, что его также можно использовать для упрощения ручного декодирования и кодирования. Например, при ручном декодировании:

struct Example : Decodable {

    var name: String
    var age: Int
    var taxRate: Float

    private enum CodingKeys: String, CodingKey {
        case name, age
        case taxRate = "tax_rate"
    }

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

        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
        self.taxRate = try container.decode(StringCodableMap<Float>.self,
                                            forKey: .taxRate).decoded
    }
}
person Hamish    schedule 16.06.2017
comment
Так что же тогда станет предложением Swift? - person Dru Freeman; 19.06.2017
comment
@LordAndrei Я бы порекомендовал поднять его в списке рассылки быстрой эволюции. Я изначально полагаю, что было бы лучше использовать его как дополнительную опцию для _1 _ / _ 2_, а не как капитальный ремонт Codable. Учитывая существующую возможность декодирования и кодирования исключительных значений с плавающей запятой в строки, это кажется естественным местом для этого. - person Hamish; 19.06.2017

Используя Swift 5.1, вы можете выбрать один из трех следующих способов решения вашей проблемы.


№1. Использование инициализатора Decodable init(from:)

Используйте эту стратегию, когда вам нужно преобразовать из String в Float для одной структуры, перечисления или класса.

import Foundation

struct ExampleJson: Decodable {

    var name: String
    var age: Int
    var taxRate: Float

    enum CodingKeys: String, CodingKey {
        case name, age, taxRate = "tax_rate"
    }

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

        name = try container.decode(String.self, forKey: CodingKeys.name)
        age = try container.decode(Int.self, forKey: CodingKeys.age)
        let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
        guard let taxRateFloat = Float(taxRateString) else {
            let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
            throw DecodingError.dataCorrupted(context)
        }
        taxRate = taxRateFloat
    }

}

Использование:

import Foundation

let jsonString = """
{
  "name": "Bob",
  "age": 25,
  "tax_rate": "4.25"
}
"""

let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
 prints:
 ▿ __lldb_expr_126.ExampleJson
   - name: "Bob"
   - age: 25
   - taxRate: 4.25
 */

№2. Использование промежуточной модели

Используйте эту стратегию, когда у вас много вложенных ключей в вашем JSON или когда вам нужно преобразовать много ключей (например, из String в Float) из вашего JSON.

import Foundation

fileprivate struct PrivateExampleJson: Decodable {

    var name: String
    var age: Int
    var taxRate: String

    enum CodingKeys: String, CodingKey {
        case name, age, taxRate = "tax_rate"
    }

}

struct ExampleJson: Decodable {

    var name: String
    var age: Int
    var taxRate: Float

    init(from decoder: Decoder) throws {
        let privateExampleJson = try PrivateExampleJson(from: decoder)

        name = privateExampleJson.name
        age = privateExampleJson.age
        guard let convertedTaxRate = Float(privateExampleJson.taxRate) else {
            let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
            throw DecodingError.dataCorrupted(context)
        }
        taxRate = convertedTaxRate
    }

}

Использование:

import Foundation

let jsonString = """
{
  "name": "Bob",
  "age": 25,
  "tax_rate": "4.25"
}
"""

let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
 prints:
 ▿ __lldb_expr_126.ExampleJson
   - name: "Bob"
   - age: 25
   - taxRate: 4.25
 */

№3. Использование метода расширения KeyedDecodingContainer

Используйте эту стратегию, когда преобразование некоторых типов ключей JSON в типы свойств вашей модели (например, String в Float) является распространенным шаблоном в вашем приложении.

import Foundation

extension KeyedDecodingContainer  {

    func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
        if let stringValue = try? self.decode(String.self, forKey: key) {
            guard let floatValue = Float(stringValue) else {
                let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
                throw DecodingError.dataCorrupted(context)
            }
            return floatValue
        } else {
            let doubleValue = try self.decode(Double.self, forKey: key)
            return Float(doubleValue)
        }
    }

}

struct ExampleJson: Decodable {

    var name: String
    var age: Int
    var taxRate: Float

    enum CodingKeys: String, CodingKey {
        case name, age, taxRate = "tax_rate"
    }

}

Использование:

import Foundation

let jsonString = """
{
    "name": "Bob",
    "age": 25,
    "tax_rate": "4.25"
}
"""

let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
 prints:
 ▿ __lldb_expr_126.ExampleJson
 - name: "Bob"
 - age: 25
 - taxRate: 4.25
 */
person Imanou Petit    schedule 18.09.2017
comment
Параметр KeyedDecodingContainer хорош, если все ваши числа с плавающей запятой представлены в виде строк. Если JSON включает в себя число с плавающей запятой, которое не имеет кавычек, вы получите ошибку декодирования, потому что KeyedDecodingContainer будет ожидать строку. - person Tom Harrington; 02.01.2018
comment
@TomHarrington Совершенно верно. Я обновлю свой ответ позже, чтобы исправить эту проблему. Спасибо. - person Imanou Petit; 04.01.2018
comment
Первый вариант сработал у меня только после того, как вынул перечисление из объявления структуры. Спасибо! - person ScottyBlades; 22.01.2018

Всегда можно раскодировать вручную. Итак, учитывая:

{
    "name": "Bob",
    "age": 25,
    "tax_rate": "4.25"
}

Ты можешь сделать:

struct Example: Codable {
    let name: String
    let age: Int
    let taxRate: Float

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        age = try values.decode(Int.self, forKey: .age)
        guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
            throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
        }
        taxRate = rate
    }

    enum CodingKeys: String, CodingKey {
        case name, age
        case taxRate = "tax_rate"
    }
}

См. Раздел Кодирование и декодирование вручную в разделе Пользовательские типы кодирования и декодирования.

Но я согласен с тем, что, похоже, должен быть более элегантный процесс преобразования строк, эквивалентный DateDecodingStrategy, учитывая, сколько источников JSON неправильно возвращают числовые значения в виде строк.

person Rob    schedule 16.06.2017
comment
Я ценю этот ответ. Я отредактировал свой исходный запрос, что я пошел по этому пути; но это в противоположном направлении от моей цели. Это полезная информация для тех, кто все еще изучает этот новый API. - person Dru Freeman; 16.06.2017

Я знаю, что это действительно поздний ответ, но я начал работать Codable только пару дней назад. И я столкнулся с похожей проблемой.

Чтобы преобразовать строку в плавающее число, вы можете записать расширение в KeyedDecodingContainer и вызвать метод в расширении из init(from decoder: Decoder){}

Для проблемы, упомянутой в этом выпуске, см. Расширение, которое я написал ниже;

extension KeyedDecodingContainer {

    func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {

        guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
            return nil
        }
        return Float(value)
    }

    func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {

        guard let valueAsString = try? decode(transformFrom, forKey: key),
            let value = Float(valueAsString) else {

            throw DecodingError.typeMismatch(
                type, 
                DecodingError.Context(
                    codingPath: codingPath, 
                    debugDescription: "Decoding of \(type) from \(transformFrom) failed"
                )
            )
        }
        return value
    }
}

Вы можете вызвать этот метод из метода init(from decoder: Decoder). См. Пример ниже;

init(from decoder: Decoder) throws {

    let container = try decoder.container(keyedBy: CodingKeys.self)

    taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
}

Фактически, вы можете использовать этот подход для преобразования любого типа данных в любой другой тип. Вы можете конвертировать string to Date, string to bool, string to float, float to int и т. Д.

На самом деле, чтобы преобразовать строку в объект Date, я предпочитаю этот подход JSONEncoder().dateEncodingStrategy, потому что, если вы напишете его правильно, вы можете включить разные форматы даты в один и тот же ответ.

Надеюсь, я помог.

Обновлен метод декодирования, чтобы возвращать необязательные по предложению @Neil.

person Suran    schedule 09.07.2018
comment
Я нашел это самым элегантным решением. Однако версия decode() не должна возвращать необязательный параметр. Я отправлю необязательную версию в качестве нового ответа. - person Neil; 18.11.2019

Вы можете использовать lazy var для преобразования свойства в другой тип:

struct ExampleJson: Decodable {
    var name: String
    var age: Int
    lazy var taxRate: Float = {
        Float(self.tax_rate)!
    }()

    private var tax_rate: String
}

Одним из недостатков этого подхода является то, что вы не можете определить константу let, если хотите получить доступ к taxRate, поскольку при первом обращении к ней вы изменяете структуру.

// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
person Code Different    schedule 16.06.2017
comment
Для меня это лучшее решение, минимализм ???? - person mrfour; 23.03.2018

Я использовал версию Сурана, но обновил ее, чтобы она возвращала необязательное значение для decode (). На мой взгляд, это самая элегантная версия. Swift 5.2.

extension KeyedDecodingContainer {

func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
    guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
        return nil
    }
    return Float(value)
}

func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
    guard let str = try? decode(transformFrom, forKey: key),
        let value = Float(str) else {
            throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
    }
    return value
}
}
person Neil    schedule 18.11.2019
comment
Выглядит хорошо. Как это будет работать как для кодирования, так и для декодирования? И могу ли я создать кучу типов (HexA, HexB, HexC и т. Д.), Привязанных к String, для принудительного преобразования различных типов в Int? У меня есть вопрос с более подробной информацией о моем варианте использования: stackoverflow.com/questions/65314663/ - person Ribena; 16.12.2020

Приведенные выше параметры относятся только к ситуации, когда заданное поле всегда является строкой. Много раз я встречал API-интерфейсы, в которых выходные данные были когда-то строкой, а иногда - числом. Итак, это мое предложение, чтобы решить эту проблему. Вы должны изменить это, чтобы генерировать исключение или установить декодированное значение равным нулю.

var json = """
{
"title": "Apple",
"id": "20"
}
""";
var jsonWithInt = """
{
"title": "Apple",
"id": 20
}
""";

struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
    var value: T
    init(from decoder: Decoder) {
        print("Decoding")
        if let container = try? decoder.singleValueContainer() {
            if let val = try? container.decode(T.self) {
                value = val
                return
            }

            if let str = try? container.decode(String.self) {
                value = T.init(str) ?? T.zero
                return
            }

        }
        value = T.zero
    }
}


struct MyData: Decodable {
    let title: String
    let _id: DecodableNumberFromStringToo<Int>

    enum CodingKeys: String, CodingKey {
        case title, _id = "id"
    }

    var id: Int {
        return _id.value
    }
}

do {
    let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)

    print(parsedJson.id)

} catch {
    print(error as? DecodingError)
}


do {
    let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)

    print(parsedJson.id)

} catch {
    print(error as? DecodingError)
}
person Géza Mikló    schedule 12.12.2019
comment
Спасибо Спасибо. Эта функция должна быть встроена в декодер (хотя не спрашивайте меня, почему сервер иногда помещает число в кавычки, а иногда нет). - person David; 08.02.2020

Как использовать JSONDecodable в Swift 4:

  1. Получите ответ JSON и создайте структуру
  2. Класс Conform Decodable в Struct
  3. Другие шаги в этом проекте GitHub, простой пример
person Ananda Aiwale    schedule 01.05.2018