Swift 4 JSON Decodable: 解码类型变更的最简单方法

29

有了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)

但这与我的想法相反,因为它迫使我必须自己完成所有的事情。


1
感谢您,@Rob。我已经修复了这个问题,解决了那个疏忽。 - Dru Freeman
5
我遇到了同样的问题,于是我打开了一个关于Swift bug的页面(链接为 https://bugs.swift.org/browse/SR-5249)。在JSON中将数字包装成字符串非常普遍,我希望Swift团队能够处理这个问题。 - chrismanderson
1
看起来Swift团队正在关注这个问题。祈祷好运! - chrismanderson
请查看我的答案,其中展示了解决您问题的最多三种不同方法。 - Imanou Petit
8个回答

20

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


#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 中有许多嵌套键,或者需要将许多键(例如从 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. 使用 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
 */

1
KeyedDecodingContainer选项只有在所有浮点数都表示为字符串时才有效。如果JSON包含一个没有引号的浮点数,您将会收到解码错误,因为KeyedDecodingContainer将期望一个字符串。 - Tom Harrington
1
@TomHarrington 完全正确。稍后我会更新我的答案来解决这个问题。谢谢。 - Imanou Petit
第一个选项对我只有在将枚举从结构声明中取出后才起作用。谢谢! - ScottyBlades
我也遇到了同样的问题,有点惊讶的是在Swift 5中还没有针对此问题的修复。我尝试了KeyedDecodingContainer扩展方法,它非常适用于我的问题。 - Guy Middleton
#3非常有帮助。
  1. decode有一个可选值的兄弟函数decodeIfPresent
  2. 在重写该函数后,要访问其"super"实现,请使用return try superDecoder(forKey: key).singleValueContainer().decode(Bool.self)
- BrianHenryIE
#3非常有帮助。
  1. decode有一个可选值的兄弟函数decodeIfPresent
  2. 在重写函数后,要访问其“super”实现,请使用return try superDecoder(forKey: key).singleValueContainer().decode(Bool.self)
- undefined

18
很遗憾,我认为在当前的JSONDecoder API中不存在这样的选项。唯一存在的选择是将异常浮点值转换为字符串表示形式,详情请参见非符合性浮点解码策略
手动解码的另一个可能的解决方案是为任何可以编码为其String表示形式并从中进行解码的LosslessStringConvertible定义一个Codable包装器类型:
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
    }
}

那这就变成了一个 Swift 提案了吗? - Dru Freeman
3
我建议在Swift演进邮件列表上提出此问题。我的初步感觉是,最好将其作为JSONDecoder/JSONEncoder的额外选项,而不是对Codable进行大规模的改动。鉴于解码和编码浮点异常值为字符串的现有选项,这似乎是一个自然的位置。 - Hamish

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源不正确地将数字值作为字符串返回。


谢谢您的回复。我已经编辑了我的原始查询,因为我已经尝试过这种方法;但是这与我的目标相反。对于那些仍在学习这个新API的人来说,这是很有用的信息。 - Dru Freeman

2

我知道这是一个很晚的答案,但我几天前才开始使用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)
}

事实上,您可以使用这种方法将任何类型的数据转换为任何其他类型。您可以将字符串转换为日期、布尔值、浮点数、浮点数转换为整数等。
实际上,要将字符串转换为日期对象,我更喜欢这种方法,而不是使用JSONEncoder().dateEncodingStrategy,因为如果您编写得当,可以在同一个响应中包含不同的日期格式。
希望我能帮到您。
根据@Neil的建议,更新了解码方法以返回非可选项。

我发现这是最优雅的解决方案。然而,decode()版本不应该返回可选项。我将发布非可选版本作为新答案。 - Neil

2

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

这看起来不错。对于编码和解码,这怎么工作?我能否创建一堆类型别名(HexA、HexB、HexC 等),将它们绑定到字符串以强制进行不同种类的转换为整数?我有一个关于我的用例更详细的问题:https://stackoverflow.com/questions/65314663/using-codable-to-encode-decode-from-strings-to-ints-with-a-function-in-between - Ribena

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
}

这种方法的一个缺点是,如果您想访问 taxRate,则无法定义一个 let 常量,因为第一次访问它时,会对结构体进行突变。
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)

这对我来说是最好的解决方案,极简主义。 - mrfour

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

谢谢。这个功能应该内置到解码器中(尽管不要问我为什么服务器有时会将数字放在引号中,有时则不会)。 - David

-6

如何在Swift 4中使用JSONDecodable:

  1. 获取JSON响应并创建结构体
  2. 在结构体中符合可解码类
  3. 其他步骤请参考这个GitHub项目,一个简单的示例

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接