27

使用 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 引入字符串并使用计算获取:

    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)

但这是相反的方向,因为它迫使我为自己做这一切。

4

8 回答 8

18

不幸的是,我认为当前JSONDecoderAPI 中不存在这样的选项。仅存在一个选项可以异常浮点值与字符串表示形式相互转换。

手动解码的另一种可能的解决方案是为任何可以对其表示进行编码和解码的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.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"
    }
}

尽管这还没有达到应有的水平——希望JSONDecoderAPI 的更高版本将包含更多自定义解码选项,或者能够在CodableAPI 本身内表达类型转换。

然而,创建包装器类型的一个优点是它也可以用于简化手动解码和编码。例如,使用手动解码:

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
    }
}
于 2017-06-16T19:03:05.673 回答
18

使用 Swift 5.1,您可以选择以下三种方式之一来解决您的问题。


#1。使用Decodable init(from:)初始化器

当您需要从 to 转换为单个结构、枚举或类时,请使用此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 转换许多键(例如 fromStringFloat)时,请使用此策略。

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
 */

#3。使用KeyedDecodingContainer扩展方法

当从某些 JSON 键的类型转换为模型的属性类型(例如Stringto Float)时使用此策略是应用程序中的常见模式。

import Foundation

extension KeyedDecodingContainer  {

    func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
        if let stringValue = try? self.decode(String.self, forKey: key) {
            guard let floatValue = Float(stringValue) else {
                let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
                throw DecodingError.dataCorrupted(context)
            }
            return floatValue
        } else {
            let doubleValue = try self.decode(Double.self, forKey: key)
            return Float(doubleValue)
        }
    }

}

struct ExampleJson: Decodable {

    var name: String
    var age: Int
    var taxRate: Float

    enum CodingKeys: String, CodingKey {
        case name, age, taxRate = "tax_rate"
    }

}

用法:

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
 */
于 2017-09-18T21:52:44.727 回答
16

您始终可以手动解码。所以,给定:

{
    "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 源错误地将数值作为字符串返回。

于 2017-06-16T18:01:09.417 回答
3

我知道这是一个非常晚的答案,但我Codable只是在几天前才开始工作。我遇到了类似的问题。

为了将字符串转换为浮点数,您可以编写扩展名KeyedDecodingContainer并调用扩展名中的方法 frominit(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 {

        guard let valueAsString = try? decode(transformFrom, forKey: key),
            let value = Float(valueAsString) else {

            throw DecodingError.typeMismatch(
                type, 
                DecodingError.Context(
                    codingPath: codingPath, 
                    debugDescription: "Decoding of \(type) from \(transformFrom) failed"
                )
            )
        }
        return value
    }
}

您可以从方法中调用此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 floatfloat to int

实际上,要将字符串转换为 Date 对象,我会更喜欢这种方法,JSONEncoder().dateEncodingStrategy因为如果你写得正确,你可以在同一个响应中包含不同的日期格式。

希望我有所帮助。

更新了解码方法以根据@Neil 的建议返回非可选方法。

于 2018-07-09T13:05:43.160 回答
2

我使用了 Suran 的版本,但对其进行了更新以返回 decode() 的非可选值。对我来说,这是最优雅的版本。斯威夫特 5.2。

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 {
    guard let str = try? decode(transformFrom, forKey: key),
        let value = Float(str) else {
            throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
    }
    return value
}
}
于 2019-11-18T20:46:08.667 回答
1

您可以使用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)
于 2017-06-16T23:25:17.420 回答
1

上面的选项只处理给定字段总是字符串的情况。很多时候,我遇到过输出曾经是字符串,其他时候是数字的 API。所以这是我解决这个问题的建议。您可以更改它以引发异常或将解码值设置为 nil。

var json = """
{
"title": "Apple",
"id": "20"
}
""";
var jsonWithInt = """
{
"title": "Apple",
"id": 20
}
""";

struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
    var value: T
    init(from decoder: Decoder) {
        print("Decoding")
        if let container = try? decoder.singleValueContainer() {
            if let val = try? container.decode(T.self) {
                value = val
                return
            }

            if let str = try? container.decode(String.self) {
                value = T.init(str) ?? T.zero
                return
            }

        }
        value = T.zero
    }
}


struct MyData: Decodable {
    let title: String
    let _id: DecodableNumberFromStringToo<Int>

    enum CodingKeys: String, CodingKey {
        case title, _id = "id"
    }

    var id: Int {
        return _id.value
    }
}

do {
    let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)

    print(parsedJson.id)

} catch {
    print(error as? DecodingError)
}


do {
    let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)

    print(parsedJson.id)

} catch {
    print(error as? DecodingError)
}
于 2019-12-12T15:18:59.507 回答
-6

如何在 Swift 4 中使用 JSONDecodable:

  1. 获取 JSON 响应并创建结构
  2. 符合结构中的可解码类
  3. 这个GitHub项目中的其他步骤,一个简单的例子
于 2018-05-01T17:38:58.830 回答