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

С протоколом Swift4 Codable существует большой уровень под капотом и страtagsи преобразования данных.

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

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

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

В противном случае я застрял в использовании 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) 

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

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

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

 struct StringCodableMap : 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 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 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 будет включать в себя более настраиваемые параметры декодирования, а также возможность выражать преобразования типов внутри самого 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.self, forKey: .taxRate).decoded } } 

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

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

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


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

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

 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. Использование промежуточной модели

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

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

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

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

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

  • Как я могу справиться с дефолтом вывода @objc с помощью #selector () в Swift 4?
  • Давайте будем гением компьютера.