Swift Codable:自定义行为的 JSONDecoder 子类
Swift Codable: subclass JSONDecoder for custom behavior
我有一个不一致的 API,可能 return String
或 Number
作为 JSON 响应的一部分。
日期也可以用 String
或 Number
相同的方式表示,但始终是 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`")
})
但是,对于我想要应用它的 Int
或 Double
类型,没有此类自定义可用。
因此,我不得不为我正在使用的每种模型类型编写 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
阶段也是如此。
我很高兴只有 Int
和 Double
类型的自定义路径,因为 T
是 Codable
并且不能保证能够使用 String
初始化一个值,但是,我当然欢迎更通用的方法。
这是我用来测试我的原型的示例 Playground 代码。它可以直接复制粘贴到 Playground 中并且工作正常。
我的目标是让 jsonsample1
和 jsonsample2
产生相同的结果。
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
来做到这一点,您最终需要从头开始重新实现整个事情,这比您尝试做的要复杂得多。
正如评论中所建议的,您可能会过得更好:
通过尝试为 .age
属性 解码 Int.self
和 String.self
并保留成功者,手动写入 Person.init(from:)
, 或
如果您需要跨多种类型重用此解决方案,您可以编写一个包装器类型用作 属性:
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
}
您也可以将 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 每次都在使用站点(你在评论中指出)
我有一个不一致的 API,可能 return String
或 Number
作为 JSON 响应的一部分。
日期也可以用 String
或 Number
相同的方式表示,但始终是 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`")
})
但是,对于我想要应用它的 Int
或 Double
类型,没有此类自定义可用。
因此,我不得不为我正在使用的每种模型类型编写 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
阶段也是如此。
我很高兴只有 Int
和 Double
类型的自定义路径,因为 T
是 Codable
并且不能保证能够使用 String
初始化一个值,但是,我当然欢迎更通用的方法。
这是我用来测试我的原型的示例 Playground 代码。它可以直接复制粘贴到 Playground 中并且工作正常。
我的目标是让 jsonsample1
和 jsonsample2
产生相同的结果。
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
来做到这一点,您最终需要从头开始重新实现整个事情,这比您尝试做的要复杂得多。
正如评论中所建议的,您可能会过得更好:
通过尝试为
.age
属性 解码Int.self
和String.self
并保留成功者,手动写入Person.init(from:)
, 或如果您需要跨多种类型重用此解决方案,您可以编写一个包装器类型用作 属性:
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 }
您也可以将
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 每次都在使用站点(你在评论中指出)