Swift 4 JSON 解码类型更改的可解码最简单方法

Swift 4 JSON Decodable simplest way to decode type change

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

日期解码策略可以将基于字符串的日期转换为日期。

有没有什么东西可以用基于 String 的 Float

否则我一直坚持使用 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)

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

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

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

参见Encoding and Decoding Custom Types中的手动编码和解码

但我同意,考虑到有多少 JSON 来源错误地 return 数值作为字符串,似乎应该有一个更优雅的字符串转换过程等同于 DateDecodingStrategy .

不幸的是,我认为当前 JSONDecoder API 中不存在这样的选项。只有一个选项可以 convert exceptional floating-point values 往返于字符串表示形式。

手动解码的另一种可能解决方案是为任何 LosslessStringConvertible 定义一个 Codable 包装器类型,它可以编码到其 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 本身的类型转换。

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

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

您可以使用 lazy var 将 属性 转换为另一种类型:

struct ExampleJson: Decodable {
    var name: String
    var age: Int
    lazy var taxRate: Float = {
        Float(self.tax_rate)!
    }()

    private var tax_rate: String
}

这种方法的一个缺点是,如果要访问 taxRate,则无法定义 let 常量,因为第一次访问它时,您正在改变结构。

// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)

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


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

当您需要将单个结构、枚举或 class 从 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 中有许多嵌套键或者当您需要从 [=55= 转换许多键(例如从 StringFloat)时使用此策略].

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 键的类型转换为模型的 属性 类型(例如 StringFloat)时使用此策略是应用程序中的常见模式.

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

如何使用 JSON可解码 Swift 4:

  1. 获取 JSON 响应并创建结构
  2. 在结构
  3. 中符合可解码class
  4. this GitHub project中的其他步骤,一个简单的例子

我知道这是一个非常晚的答案,但我只是在几天前才开始研究 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 {

        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 Datestring to boolstring to floatfloat to int

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

希望我有所帮助。

根据 @Neil 的建议将解码方法更新为 return 非可选。

我使用了 Suran 的版本,但将其更新为 return decode() 的非可选值。对我来说,这是最优雅的版本。 Swift5.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
}
}

以上选项仅处理给定字段始终为字符串的情况。很多次我遇到过输出一次是字符串,有时是数字的 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)
}