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

Posted

技术标签:

【中文标题】Swift 4 JSON Decodable 解码类型更改的最简单方法【英文标题】:Swift 4 JSON Decodable simplest way to decode type change 【发布时间】:2017-11-19 13:12:13 【问题描述】:

借助 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)

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

【问题讨论】:

谢谢@Rob,我用这个疏忽解决了这个问题。 我遇到了同样的问题并打开了一个 !Swift bug。在 JSON 中将数字包装为字符串是如此普遍,我希望 Swift 团队能够处理这种情况。 看来 Swift 团队正在研究这个问题。手指交叉! 请参阅my answer,其中显示了最多 3 种不同的方法来解决您的问题。 【参考方案1】:

不幸的是,我认为当前的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
    

【讨论】:

那么这会成为一个 Swift 提案吗? @LordAndrei 我建议在swift evolution mailing list 上提出它。我最初的感觉是,最好将它作为JSONDecoder/JSONEncoder 的额外选项,而不是作为Codable 的大修。鉴于现有的将异常浮点值解码和编码为字符串的选项,它似乎是一个很自然的地方。【参考方案2】:

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


#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 转换许多键(例如,从 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
 */

【讨论】:

KeyedDecodingContainer 选项很好,只要您的 所有 浮点数都表示为字符串。如果 JSON 包含一个不带引号的浮点数,则会出现解码错误,因为 KeyedDecodingContainer 将期待一个字符串。 @TomHarrington 完全正确。我稍后会更新我的答案以解决此问题。谢谢。 第一个选项只有在将枚举从结构声明中取出后才对我有用。谢谢!【参考方案3】:

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


    "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 源错误地将数值返回为字符串,似乎应该有一个更优雅的字符串转换过程,相当于 DateDecodingStrategy

【讨论】:

感谢您的回复。我已经编辑了我的原始查询,我走了这条路;但这与我的目标相反。对于仍在学习这个新 API 的人来说,这是一个很好的信息。【参考方案4】:

我知道这是一个很晚的答案,但我几天前才开始研究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 等。

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

希望我能帮上忙。

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

【讨论】:

我发现这是最优雅的解决方案。但是,decode() 版本不应返回可选项。我将发布非可选版本作为新答案。【参考方案5】:

我使用了 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


【讨论】:

这看起来不错。这将如何用于编码和解码?我可以创建一堆与 String 绑定的类型别名(HexA、HexB、HexC 等)以强制将不同类型的转换为 Int 吗?我有一个关于我的用例的更多详细信息的问题:***.com/questions/65314663/…【参考方案6】:

您可以使用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)

【讨论】:

对我来说是最好的解决方案,极简主义?【参考方案7】:

上面的选项只处理给定字段总是字符串的情况。很多时候,我遇到过输出曾经是字符串,其他时候是数字的 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)

【讨论】:

谢谢谢谢。这个功能应该内置在解码器中(尽管不要问我为什么服务器有时会在引号中加上数字,有时不会)。【参考方案8】:

如何在 Swift 4 中使用 JSONDecodable:

    获取 JSON 响应并创建结构 在 Struct 中符合 Decodable 类 this GitHub project中的其他步骤,一个简单的例子

【讨论】:

以上是关于Swift 4 JSON Decodable 解码类型更改的最简单方法的主要内容,如果未能解决你的问题,请参考以下文章

Swift Decodable - 如何解码经过 base64 编码的嵌套 JSON

使用 Swift 4 Decodable 将字符串 JSON 响应转换为布尔值

Swift 4 Decodable - 没有与键 CodingKeys 关联的值 [重复]

使用 Swift 4 的 Decodable 解码 Void

仅在运行时知道类型时的 Swift 4 JSON 解码

Swift 4 可解码 - 附加变量