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) Другие шаги в следующем проекте (простой пример)