如何使 Swift Codable 类型更加通用
How to make Swift Codable types more versatile
我目前正在使用处理公交车预测的 API。 JSON 有一个有趣的怪癖,它会返回一个特定停止的预测。当有多个停止预测时,JSON 看起来像这样:
...
"direction": {
"prediction": [
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571787798536",
"isDeparture": "false",
"minutes": "50",
"seconds": "3008",
"tripTag": "122",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571789598536",
"isDeparture": "false",
"minutes": "80",
"seconds": "4808",
"tripTag": "123",
"vehicle": "1698"
}
],
"title": "Loop"
}
...
但是,当只有一个停止预测时,JSON 看起来像这样:
...
"direction": {
"prediction":
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
}
"title": "Loop"
}
...
请注意,"prediction" 不再位于数组内——这是我认为使用 Swift Codable 类型解码 JSON 时事情变得复杂的地方。对于 "direction" 和 "prediction"
,我的模型看起来像这样
struct BTDirection: Codable {
let title: String!
let stopTitle: String!
let prediction: [BTPrediction]!
}
struct BTPrediction: Codable {
let minutes: String!
let vehicle: String!
}
基本上发生的事情是 prediction
in BTDirection
正在寻找一个 BTPrediction 数组,但是在上面的第二种情况下,这不是一个数组,因此解码失败。如何使我的模型更灵活以适应数组或单个对象?理想情况下,在第二种情况下 prediction
仍然是单个 BTDirection
的数组。如有任何帮助,我们将不胜感激。
你可以试试
struct BTDirection:Codable {
let title,stopTitle: String
let prediction: [BTPrediction]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decode(String.self, forKey: .stopTitle)
do {
let res = try container.decode([BTPrediction].self, forKey: .prediction)
prediction = res
}
catch {
let res = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [res]
}
}
}
为了补充 Sh_Khan 的答案,如果您的 API 响应中有多个地方发生了此类事情,您可以将此自定义解码和编码提取到自定义包装器类型,因此您不必在任何地方重复它,例如:
/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
private var elements: [Element]
}
extension ArrayOrSingleItem: Decodable where Element: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// First try decoding the single value as an array of `Element`s.
elements = try container.decode([Element].self)
} catch {
// If decoding as an array of `Element`s didn't work, try decoding
// the single value as a single `Element`, and store it in an array.
elements = try [container.decode(Element.self)]
}
}
}
extension ArrayOrSingleItem: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if elements.count == 1, let element = elements.first {
// If the wrapped array of `Element`s has exactly one `Element`
// in it, encode this as just that one `Element`.
try container.encode(element)
} else {
// Otherwise, encode the wrapped array just as it is - an array
// of `Element`s.
try container.encode(elements)
}
}
}
// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
// let directions: ArrayOrSingleItem<BTDirection> = ...
// let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
subscript(position: Int) -> Element {
get { elements[position] }
set { elements[position] = newValue }
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
func index(after i: Int) -> Int {
elements.index(after: i)
}
}
// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.elements = elements
}
}
然后您可以像这样声明您的 prediction
(以及任何其他可能是数组或 API 响应中的单个项目的 属性):
struct BTDirection: Codable {
let title: String?
let stopTitle: String?
let prediction: ArrayOrSingleItem<BTPrediction>?
}
@TylerTheCompiler 和@Sh_Khan 都为提供解决方案机制的解决方案提供了非常好的技术输入,但提供的代码会遇到给定 json 数据的一些实施问题:
- 发布的 JSON 中存在错误,将导致 codable 停止使用它 - 我怀疑这些只是复制和粘贴错误,但如果不是,您将遇到问题。
- 由于初始
direction
键 JSON 实际上有 3(或至少 2.5!)层嵌套。这将需要在 init(from:)
中展平,或者如下所示,需要一个临时结构以便于映射。在初始化程序中展平会更优雅,临时结构要快得多:-)
- CodingKeys,虽然很明显,但在之前的答案中没有定义,所以会导致编译 init(from:) 的错误
- JSON 中没有
stopTitle
字段,因此解码时会出错,除非将其视为可选字段。这里我把它当成一个具体的String
,在解码的时候处理了;你可以把它变成 String?
然后解码器会处理它的缺失。
使用 "corrected" JSON(添加左大括号、缺少逗号等)以下代码将导入这两种情况。我还没有实现 arrayOrSingleItem
因为所有功劳都属于@TylerTheCompiler,但你可以很容易地把它放进去。
struct Direction: Decodable {
let direction: BTDirection
}
struct BTDirection: Decodable {
enum CodingKeys: String, CodingKey {
case title
case stopTitle
case prediction
}
let prediction: [BTPrediction]
let title: String
let stopTitle: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
prediction = try container.decode([BTPrediction].self, forKey: .prediction)
} catch {
let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [singlePrediction]
}
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
}
}
struct BTPrediction: Decodable {
let minutes: String
let vehicle: String
}
然后实际解码 JSON 解码顶级方向类型
let data = json.data(using: .utf8)
if let data = data {
do {
let bus = try decoder.decode(Direction.self, from: data)
// extract the BTDirection type from the temporary Direction type
// and do something with the decoded data
}catch {
//handle error
}
}
如果您不知道,JSON Validator 对 validating/correcting json 非常有用。
我目前正在使用处理公交车预测的 API。 JSON 有一个有趣的怪癖,它会返回一个特定停止的预测。当有多个停止预测时,JSON 看起来像这样:
...
"direction": {
"prediction": [
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571787798536",
"isDeparture": "false",
"minutes": "50",
"seconds": "3008",
"tripTag": "122",
"vehicle": "1698"
},
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571789598536",
"isDeparture": "false",
"minutes": "80",
"seconds": "4808",
"tripTag": "123",
"vehicle": "1698"
}
],
"title": "Loop"
}
...
但是,当只有一个停止预测时,JSON 看起来像这样:
...
"direction": {
"prediction":
{
"affectedByLayover": "true",
"block": "241",
"dirTag": "loop",
"epochTime": "1571785998536",
"isDeparture": "false",
"minutes": "20",
"seconds": "1208",
"tripTag": "121",
"vehicle": "1698"
}
"title": "Loop"
}
...
请注意,"prediction" 不再位于数组内——这是我认为使用 Swift Codable 类型解码 JSON 时事情变得复杂的地方。对于 "direction" 和 "prediction"
,我的模型看起来像这样struct BTDirection: Codable {
let title: String!
let stopTitle: String!
let prediction: [BTPrediction]!
}
struct BTPrediction: Codable {
let minutes: String!
let vehicle: String!
}
基本上发生的事情是 prediction
in BTDirection
正在寻找一个 BTPrediction 数组,但是在上面的第二种情况下,这不是一个数组,因此解码失败。如何使我的模型更灵活以适应数组或单个对象?理想情况下,在第二种情况下 prediction
仍然是单个 BTDirection
的数组。如有任何帮助,我们将不胜感激。
你可以试试
struct BTDirection:Codable {
let title,stopTitle: String
let prediction: [BTPrediction]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decode(String.self, forKey: .stopTitle)
do {
let res = try container.decode([BTPrediction].self, forKey: .prediction)
prediction = res
}
catch {
let res = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [res]
}
}
}
为了补充 Sh_Khan 的答案,如果您的 API 响应中有多个地方发生了此类事情,您可以将此自定义解码和编码提取到自定义包装器类型,因此您不必在任何地方重复它,例如:
/// Wrapper type that can be encoded/decoded to/from either
/// an array of `Element`s or a single `Element`.
struct ArrayOrSingleItem<Element> {
private var elements: [Element]
}
extension ArrayOrSingleItem: Decodable where Element: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
// First try decoding the single value as an array of `Element`s.
elements = try container.decode([Element].self)
} catch {
// If decoding as an array of `Element`s didn't work, try decoding
// the single value as a single `Element`, and store it in an array.
elements = try [container.decode(Element.self)]
}
}
}
extension ArrayOrSingleItem: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if elements.count == 1, let element = elements.first {
// If the wrapped array of `Element`s has exactly one `Element`
// in it, encode this as just that one `Element`.
try container.encode(element)
} else {
// Otherwise, encode the wrapped array just as it is - an array
// of `Element`s.
try container.encode(elements)
}
}
}
// This lets you treat an `ArrayOrSingleItem` like a collection of elements.
// If you need the elements as type `Array<Element>`, just instantiate a new
// `Array` from your `ArrayOrSingleItem` like:
// let directions: ArrayOrSingleItem<BTDirection> = ...
// let array: [BTDirection] = Array(directions)
extension ArrayOrSingleItem: MutableCollection {
subscript(position: Int) -> Element {
get { elements[position] }
set { elements[position] = newValue }
}
var startIndex: Int { elements.startIndex }
var endIndex: Int { elements.endIndex }
func index(after i: Int) -> Int {
elements.index(after: i)
}
}
// This lets you instantiate an `ArrayOrSingleItem` from an `Array` literal.
extension ArrayOrSingleItem: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.elements = elements
}
}
然后您可以像这样声明您的 prediction
(以及任何其他可能是数组或 API 响应中的单个项目的 属性):
struct BTDirection: Codable {
let title: String?
let stopTitle: String?
let prediction: ArrayOrSingleItem<BTPrediction>?
}
@TylerTheCompiler 和@Sh_Khan 都为提供解决方案机制的解决方案提供了非常好的技术输入,但提供的代码会遇到给定 json 数据的一些实施问题:
- 发布的 JSON 中存在错误,将导致 codable 停止使用它 - 我怀疑这些只是复制和粘贴错误,但如果不是,您将遇到问题。
- 由于初始
direction
键 JSON 实际上有 3(或至少 2.5!)层嵌套。这将需要在init(from:)
中展平,或者如下所示,需要一个临时结构以便于映射。在初始化程序中展平会更优雅,临时结构要快得多:-) - CodingKeys,虽然很明显,但在之前的答案中没有定义,所以会导致编译 init(from:) 的错误
- JSON 中没有
stopTitle
字段,因此解码时会出错,除非将其视为可选字段。这里我把它当成一个具体的String
,在解码的时候处理了;你可以把它变成String?
然后解码器会处理它的缺失。
使用 "corrected" JSON(添加左大括号、缺少逗号等)以下代码将导入这两种情况。我还没有实现 arrayOrSingleItem
因为所有功劳都属于@TylerTheCompiler,但你可以很容易地把它放进去。
struct Direction: Decodable {
let direction: BTDirection
}
struct BTDirection: Decodable {
enum CodingKeys: String, CodingKey {
case title
case stopTitle
case prediction
}
let prediction: [BTPrediction]
let title: String
let stopTitle: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
prediction = try container.decode([BTPrediction].self, forKey: .prediction)
} catch {
let singlePrediction = try container.decode(BTPrediction.self, forKey: .prediction)
prediction = [singlePrediction]
}
title = try container.decode(String.self, forKey: .title)
stopTitle = try container.decodeIfPresent(String.self, forKey: .stopTitle) ?? "unnamed stop"
}
}
struct BTPrediction: Decodable {
let minutes: String
let vehicle: String
}
然后实际解码 JSON 解码顶级方向类型
let data = json.data(using: .utf8)
if let data = data {
do {
let bus = try decoder.decode(Direction.self, from: data)
// extract the BTDirection type from the temporary Direction type
// and do something with the decoded data
}catch {
//handle error
}
}
如果您不知道,JSON Validator 对 validating/correcting json 非常有用。