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"
}
}
日付デコード戦略は、文字列ベースの日付を日付に変換できます。
文字列ベースのフロートでそれを行うものはありますか
そうでなければ、私はCodingKeyを使用して文字列を取り込み、コンピューティング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)
しかし、それは逆の方向にあり、それは私が自分自身のためにそれをすべてやらなければならないからです。
残念ながら、現在のJSONDecoder
APIにはそのようなオプションが存在するとは思わない。 convertexceptional浮動小数点値 と文字列表現との間のオプションのみが存在します。
手動でデコードする別の可能な解決策は、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自体。
ただし、ラッパータイプを作成する利点の1つは、手動でのデコードとエンコードをより簡単にするためにも使用できることです。たとえば、手動デコードの場合:
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
}
}
いつでも手動でデコードできます。だから、与えられた:
{
"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
に相当するよりエレガントな文字列変換プロセスが必要であるように見えることです。
ニーズに応じて、問題を解決するために次の2つの方法のいずれかを選択できます。
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
*/
JSONに多くのネストされたキーがある場合、またはJSONから多くのキー(String
からFloat
など)を変換する必要がある場合に、この戦略を使用します。
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
}
このアプローチの欠点の1つは、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
よりもこの方法をお勧めします。適切に記述すれば、同じ応答に異なる日付形式を含めることができるからです。
お役に立てば幸いです。