Swift Codable:自定义行为的 JSONDecoder 子类

Swift Codable: subclass JSONDecoder for custom behavior

我有一个不一致的 API,可能 return StringNumber 作为 JSON 响应的一部分。

日期也可以用 StringNumber 相同的方式表示,但始终是 UNIX 时间戳(即 timeIntervalSince1970)。

为了解决日期问题,我只是使用了自定义 JSONDecoder.DateDecodingStrategy:

decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom({ decoder in
            let container = try decoder.singleValueContainer()
            if let doubleValue = try? container.decode(Double.self) {
                return Date(timeIntervalSince1970: doubleValue)
            } else if let stringValue = try? container.decode(String.self),
                      let doubleValue = Double(stringValue) {
                return Date(timeIntervalSince1970: doubleValue)
            }
            
            throw DecodingError.dataCorruptedError(in: container,
                                                   debugDescription: "Unable to decode value of type `Date`")
        })

但是,对于我想要应用它的 IntDouble 类型,没有此类自定义可用。

因此,我不得不为我正在使用的每种模型类型编写 Codable 初始化程序。

我正在寻找的替代方法是继承 JSONDecoder 并覆盖 decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable 方法。

在该方法中,我想“检查”我尝试解码的类型 T,然后,如果基本实现 (super) 失败,请尝试解码该值首先为 String,然后为 T(目标类型)。

到目前为止,我的初始原型如下所示:

final class CustomDecoder: JSONDecoder {
    override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
        do {
            return try super.decode(type, from: data)
        } catch {
            if type is Int.Type {
                print("Trying to decode as a String")
                if let decoded = try? super.decode(String.self, from: data),
                   let converted = Int(decoded) {
                    return converted as! T
                }
            }
            throw error
        }
    }
}

但是,我发现由于某种原因,"Trying to decode as a String" 消息从未打印出来,即使控件到达 catch 阶段也是如此。

我很高兴只有 IntDouble 类型的自定义路径,因为 TCodable 并且不能保证能够使用 String 初始化一个值,但是,我当然欢迎更通用的方法。

这是我用来测试我的原型的示例 Playground 代码。它可以直接复制粘贴到 Playground 中并且工作正常。 我的目标是让 jsonsample1jsonsample2 产生相同的结果。

import UIKit


final class CustomDecoder: JSONDecoder {
    override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
        do {
            return try super.decode(type, from: data)
        } catch {
            if type is Int.Type {
                print("Trying to decode as a String")
                if let decoded = try? super.decode(String.self, from: data),
                   let converted = Int(decoded) {
                    return converted as! T
                }
            }
            throw error
        }
    }
}

let jsonSample1 =
"""
    {
        "name": "Paul",
        "age": "38"
    }
"""

let jsonSample2 =
"""
    {
        "name": "Paul",
        "age": 38
    }
"""

let data1 = jsonSample1.data(using: .utf8)!
let data2 = jsonSample2.data(using: .utf8)!


struct Person: Codable {
    let name: String?
    let age: Int?
}


let decoder = CustomDecoder()
let person1 = try? decoder.decode(Person.self, from: data1)
let person2 = try? decoder.decode(Person.self, from: data2)
print(person1 as Any)
print(person2 as Any)

我的 CustomDecoder 无法正常工作的原因可能是什么?

您的解码器没有按照您的预期执行的主要原因是您没有覆盖您想要成为的方法:JSONDecoder.decode<T>(_:from:)top-level 调用时调用的方法

try JSONDecoder().decode(Person.self, from: data)

但这不是在解码期间内部调用的方法。以您显示的 JSON 为例,如果我们将 Person 结构写为

struct Person: Decodable {
   let name: String
   let age: Int
}

然后编译器将编写一个 init(from:) 方法,如下所示:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decode(Int.self, forKey: .age)
}

请注意,当我们解码 age 时,我们并不是直接在解码器上调用方法,而是在我们从解码器获得的 KeyedCodingContainer 上——具体来说,Int.Type KeyedDecodingContainer.decode(_:forKey:).

过载

为了在 Decoder 的中间级别挂钩解码期间调用的方法,您需要挂钩其实际容器方法,这非常困难 — 所有 JSONDecoder 的容器和内部结构是私有的。为了通过子类化 JSONDecoder 来做到这一点,您最终需要从头开始重新实现整个事情,这比您尝试做的要复杂得多。


正如评论中所建议的,您可能会过得更好:

  1. 通过尝试为 .age 属性 解码 Int.selfString.self 并保留成功者,手动写入 Person.init(from:) , 或

  2. 如果您需要跨多种类型重用此解决方案,您可以编写一个包装器类型用作 属性:

    struct StringOrNumber: Decodable {
        let number: Double
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            do {
                number = try container.decode(Double.self)
            } catch (DecodingError.typeMismatch) {
                let string = try container.decode(String.self)
                if let n = Double(string) {
                    number = n
                } else {
                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
                }
            }
        }
    }
    
    struct Person: Decodable {
        let name: String
        let age: StringOrNumber
    }
    
  3. 您也可以将 StringOrNumber 写成 enum,如果知道有效负载很重要:

    enum StringOrNumber: Decodable {
        case number(Double)
        case string(String)
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            do {
                self = try .number(container.decode(Double.self))
            } catch (DecodingError.typeMismatch) {
                let string = try container.decode(String.self)
                if let n = Double(string) {
                    self = .string(string)
                } else {
                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
                }
            }
        }
    }
    

    尽管如果您总是需要Double/Int访问数据,那么这就没有那么重要了,因为您需要re-convert 每次都在使用站点(你在评论中指出)