Swift 4 JSON Декодируемый простейший способ декодирования изменения типа

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

Учитывая 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"
    }
}

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

Есть ли что-то, что делает это с помощью Float на основе String?

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

    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)

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

Ответ 1

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

Другим возможным решением для декодирования вручную является определение типа оболочки 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"
    }
}

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

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

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

Ответ 2

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

{
    "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 там неверно возвращают числовые значения в виде строк.

Ответ 3

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


# 1. Использование Decodable init(from:) initializer

Используйте эту стратегию, когда вам нужно преобразовать из 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 to 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
 */

Ответ 4

Вы можете использовать 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)

Ответ 5

Я знаю, что это очень поздний ответ, но я начал работать только на 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? {

         return Float(try decode(transformFrom, forKey: key))
    }
}

Вы можете вызвать этот метод из метода 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 потому что, если вы его правильно напишете, вы можете включить разные форматы дат в один и тот же ответ.

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

Ответ 6

введите ссылку здесь здесь. Как использовать JSONDecodable в Swift4
1) получить JSON Response и Create Struct 2) соответствовать классу Decodable в Struct 3) Другие шаги в следующем проекте (простой пример)