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

时间:2017-06-16 17:11:03

标签: json swift swift4 codable

使用swift4的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引入一个String并使用计算得到:

    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)

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

8 个答案:

答案 0 :(得分:17)

不幸的是,我不相信当前JSONDecoder API中存在这样的选项。只有convert exceptional floating-point values来自字符串表示的选项。

手动解码的另一种可能解决方案是为任何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的更高版本将包含更多自定义解码选项,或者能够在{{{{}}内表达类型转换1}} API本身。

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

Codable

答案 1 :(得分:14)

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

{
    "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中的手动编码和解码

但我同意,似乎应该有一个更优雅的字符串转换过程等效于DateDecodingStrategy,因为有多少JSON源错误地将数值作为字符串返回。

答案 2 :(得分:7)

根据您的需要,您可以选择以下两种方法之一来解决您的问题。

#1。使用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
 */

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

答案 3 :(得分:1)

您可以使用delete static_cast<Test1*>(t1); 将属性转换为其他类型:

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

答案 4 :(得分:1)

我知道这是一个很晚的答案,但是仅几天前我才开始进行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 Datestring to boolstring to floatfloat to int等。

实际上是将字符串转换为Date对象,与JSONEncoder().dateEncodingStrategy相比,我更喜欢这种方法,因为如果正确编写,则可以在同一响应中包括不同的日期格式。

希望我能帮上忙。

答案 5 :(得分:1)

我使用了Suran的版本,但对其进行了更新,以返回crypto()的非可选值。对我来说,这是最优雅的版本。 Swift 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
}
}

答案 6 :(得分:0)

以上选项仅处理给定字段始终为String的情况。我遇到过很多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)
}

答案 7 :(得分:-3)

enter link description here如何在Swift4中使用JSONDecodable
1)获取JSON响应和创建结构 2)符合Struct中的Decodable类 3)以下项目中的其他步骤(简单示例)