web-dev-qa-db-ja.com

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

日付デコード戦略は、文字列ベースの日付を日付に変換できます。

文字列ベースのフロートでそれを行うものはありますか

そうでなければ、私はCodi​​ngKeyを使用して文字列を取り込み、コンピューティング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)

しかし、それは逆の方向にあり、それは私が自分自身のためにそれをすべてやらなければならないからです。

24
Dru Freeman

残念ながら、現在のJSONDecoder AP​​Iにはそのようなオプションが存在するとは思わない。 convertexceptional浮動小数点値 と文字列表現との間のオプションのみが存在します。

手動でデコードする別の可能な解決策は、Codable表現にエンコードおよびデコードできるLosslessStringConvertibleStringラッパータイプを定義することです。

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.decodedFloat値と対話するため。

ただし、これを軽減するために、単純な転送計算プロパティを常に定義できます。

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 AP​​Iの新しいバージョンには、より多くのカスタムデコードオプションが含まれるか、Codable AP​​I自体。

ただし、ラッパータイプを作成する利点の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
    }
}
18
Hamish

いつでも手動でデコードできます。だから、与えられた:

{
    "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に相当するよりエレガントな文字列変換プロセスが必要であるように見えることです。

14
Rob

ニーズに応じて、問題を解決するために次の2つの方法のいずれかを選択できます。


#1。 Decodableinit(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に多くのネストされたキーがある場合、または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
 */
7
Imanou Petit

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)
1
Code Different

これが本当に遅い答えであることは知っていますが、数日前まで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よりもこの方法をお勧めします。適切に記述すれば、同じ応答に異なる日付形式を含めることができるからです。

お役に立てば幸いです。

1
Suran