JSON 从 Swift 中的递归枚举解码和编码
JSON decode to and encode from recursive enum in Swift
我有一个 JSON 文件,看起来像这样:
{
"items" : [
{ "name": "a name", "version": "a version" },
{ "version": "a 2nd version" },
{
"any_of": [
{ "name": "some name" },
{ "name": "some other name", "version": "some other version" },
[
{ "name": "another name" },
{ "version": "another version" },
{
"any_of": [
[
{ "version": "some version" },
{ "version": "some version" }
],
{ "version": "yet another version" }
]
}
]
]
},
{
"any_of" : [
{ "name": "a name" },
{ "name": "another name" }
]
}
]
}
JSON 文件具有递归结构。 any_of
键表示其数组中所有元素之间存在 OR
关系,缺少 any_of
键表示存在 AND
关系。我想使用 Swift 的 Codable
协议解码(和编码)JSON 文件,目前我有一个 Codable
结构代表 name
version
JSON 对象:
struct NameVersion: Codable {
let name: String?
let version: String?
func toString() -> String { "\(name ?? "")\(version ?? "")" }
}
和代表整个 JSON 结构的 Codable
枚举:
enum Items: Codable {
init(from decoder: Decoder) throws {
// decode from JSON here
}
func encode(to encoder: Encoder) throws {
// encode to JSON here
}
case item(NameVersion)
// A set of `Items` instances with an "OR" relationship.
// This represents a JSON array with an "any_of" key.
case anyOfItems(Set<Items>)
// A set of `Items` instances with an "AND" relationship
// This represents a JSON array without an "any_of" key.
case allOfItems(Set<Items>)
// This function might help illustrate the structure and my goal.
func toString() -> String {
switch self {
case let .item(item):
return item.toString()
case let .anyOfItems(items):
return "(\(items.map { [=14=].toString() }.joined(separator: " ∨ ")))"
case let .allOfItems(items):
return "(\(items.map { [=14=].toString() }.joined(separator: " ∧ ")))"
}
}
}
我在为 Items
枚举实现 init(from:)
和 encode(to:)
函数时遇到问题。我检查了 Stack Overflow 问题 ,但我的情况与它不同,我的枚举没有嵌套在结构中,并且我的 item
的 NameVersion
类型的关联值没有出现直接来自键值对。
几乎尝试了所有方法后,我发现解码 JSON 的最佳方法是通过 UnkeyedDecodingContainer
protocol.
根据文档,未加密的容器 "is used to hold the encoded properties of a decodable type sequentially, without keys." 这描述了给定 JSON 结构的完美匹配。
因为Codable
只是Decodable
和Encodable
的别名,所以让Items
在Encodable
之前先符合Decodable
。
Decodable
一致性
鉴于此 Codable
结构包含底层 JSON 对象:
struct NameVersion: Codable {
let name: String?
let version: String?
}
解码JSON:
indirect enum Items: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
//
// This initialiser is designed recursively decode nested JSON arrays into
// recursive Swift enums, so we need an instance of a collection type to
// hold all the intermediate results.
//
// Because the unkeyed values in JSON are in a sequence, and because 2 of
// Item's cases have associated values of Set<Items> type, we need a
// Set<Items> variable to hold all the values while the JSON values are
// decoded one by one.
var itemsSet: Set<Items> = []
// Create an unkeyed container holding the current level of JSON values.
var unkeyedValues = try decoder.unkeyedContainer()
// "Loop" through values in the unkeyed container.
// The unkeyed container does not conform to the `Sequence` protocol,
// but its `currentIndex` property grows by 1 every time when a value
// is decoded successfully.
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
// Case 1: the JSON value decodes to a NameVersion instance.
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
}
// Case 2: the JSON value is a { "any_of": [] } object.
// This requires a service structure to take care of it.
// More detailed explanation on this later.
else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
}
// Case 3: the JSON value is an array without a key.
else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
// If the unkeyed container's current index didn't increase by 1
// during this loop, then the the unkeyed value at the current index
// was not decoded, and will not be in future loops. There is no way
// to increment the index manually, so the unkeyed container will keep
// trying for the same value. The only choice is to break out of the
// loop in this situation.
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
// If there is only 1 Item in the set, we can just assign it to self.
self = ItemsSet.popFirst()!
} else {
// Since all "any_of" JSON arrays are taken care of by the service
// structure, all Items instances in the set are decoded from an
// unkeyed JSON array.
self = .allOfItems(itemsSet)
}
}
func encode(to encoder: Encoder) throws {
// TODO: encode to JSON here
}
case item(NameVersion)
// A set of `Items` instances with an "OR" relationship.
// This represents a JSON array with an "any_of" key.
case anyOfItems(Set<Items>)
// A set of `Item` instances with an "AND" relationship
// This represents a JSON array without an "any_of" key.
case allOfItems(Set<Items>)
}
尽管存在一种 .nestedContainer()
方法用于从非键控容器中获取嵌套键控容器,该容器保存 { "any_of": [] }
JSON 对象的数据,但嵌套容器无法调用 decode(forKey:, from:)
方法解码 JSON.
相反,我按照 this solution 解码嵌套数据,并创建了以下服务结构来解码 { "any_of": [] }
JSON 对象。
struct AnyOfItems: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
// The decoding part for AnyOfItems is largely the same as that for
// Items, but they differ in that for AnyOfItems, the set of Items
// are given to the .anyOfItems case.
itsms = Items.anyOfItems(itemsSet)
}
}
let items: Items
}
大部分重复代码可以提取到自己的函数:
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
// Still has to be a variable, because .popFirst() is a mutating method.
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { self = ItemsSet.popFirst()! }
else { self = .allOfItems(itemsSet) }
}
func encode(to encoder: Encoder) throws {
// TODO: encode to JSON here
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { items = itemsSet.popFirst()! }
else { items = Items.anyOfItems(itemsSet) }
}
let items: Items
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}
Encodable
一致性
编码更简单。
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
// JSON decoded here
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
// JSON decoded here
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
/**
A memberwise initialiser.
*/
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
Codable
一致性
最后,所有东西都放在一起:
indirect enum Items: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
self = ItemsSet.popFirst()!
} else {
self = .allOfItems(itemsSet)
}
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
items = Items.anyOfItems(itemsSet)
}
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
/**
A memberwise initialiser.
*/
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}
我有一个 JSON 文件,看起来像这样:
{
"items" : [
{ "name": "a name", "version": "a version" },
{ "version": "a 2nd version" },
{
"any_of": [
{ "name": "some name" },
{ "name": "some other name", "version": "some other version" },
[
{ "name": "another name" },
{ "version": "another version" },
{
"any_of": [
[
{ "version": "some version" },
{ "version": "some version" }
],
{ "version": "yet another version" }
]
}
]
]
},
{
"any_of" : [
{ "name": "a name" },
{ "name": "another name" }
]
}
]
}
JSON 文件具有递归结构。 any_of
键表示其数组中所有元素之间存在 OR
关系,缺少 any_of
键表示存在 AND
关系。我想使用 Swift 的 Codable
协议解码(和编码)JSON 文件,目前我有一个 Codable
结构代表 name
version
JSON 对象:
struct NameVersion: Codable {
let name: String?
let version: String?
func toString() -> String { "\(name ?? "")\(version ?? "")" }
}
和代表整个 JSON 结构的 Codable
枚举:
enum Items: Codable {
init(from decoder: Decoder) throws {
// decode from JSON here
}
func encode(to encoder: Encoder) throws {
// encode to JSON here
}
case item(NameVersion)
// A set of `Items` instances with an "OR" relationship.
// This represents a JSON array with an "any_of" key.
case anyOfItems(Set<Items>)
// A set of `Items` instances with an "AND" relationship
// This represents a JSON array without an "any_of" key.
case allOfItems(Set<Items>)
// This function might help illustrate the structure and my goal.
func toString() -> String {
switch self {
case let .item(item):
return item.toString()
case let .anyOfItems(items):
return "(\(items.map { [=14=].toString() }.joined(separator: " ∨ ")))"
case let .allOfItems(items):
return "(\(items.map { [=14=].toString() }.joined(separator: " ∧ ")))"
}
}
}
我在为 Items
枚举实现 init(from:)
和 encode(to:)
函数时遇到问题。我检查了 Stack Overflow 问题 item
的 NameVersion
类型的关联值没有出现直接来自键值对。
几乎尝试了所有方法后,我发现解码 JSON 的最佳方法是通过 UnkeyedDecodingContainer
protocol.
根据文档,未加密的容器 "is used to hold the encoded properties of a decodable type sequentially, without keys." 这描述了给定 JSON 结构的完美匹配。
因为Codable
只是Decodable
和Encodable
的别名,所以让Items
在Encodable
之前先符合Decodable
。
Decodable
一致性
鉴于此 Codable
结构包含底层 JSON 对象:
struct NameVersion: Codable {
let name: String?
let version: String?
}
解码JSON:
indirect enum Items: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
//
// This initialiser is designed recursively decode nested JSON arrays into
// recursive Swift enums, so we need an instance of a collection type to
// hold all the intermediate results.
//
// Because the unkeyed values in JSON are in a sequence, and because 2 of
// Item's cases have associated values of Set<Items> type, we need a
// Set<Items> variable to hold all the values while the JSON values are
// decoded one by one.
var itemsSet: Set<Items> = []
// Create an unkeyed container holding the current level of JSON values.
var unkeyedValues = try decoder.unkeyedContainer()
// "Loop" through values in the unkeyed container.
// The unkeyed container does not conform to the `Sequence` protocol,
// but its `currentIndex` property grows by 1 every time when a value
// is decoded successfully.
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
// Case 1: the JSON value decodes to a NameVersion instance.
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
}
// Case 2: the JSON value is a { "any_of": [] } object.
// This requires a service structure to take care of it.
// More detailed explanation on this later.
else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
}
// Case 3: the JSON value is an array without a key.
else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
// If the unkeyed container's current index didn't increase by 1
// during this loop, then the the unkeyed value at the current index
// was not decoded, and will not be in future loops. There is no way
// to increment the index manually, so the unkeyed container will keep
// trying for the same value. The only choice is to break out of the
// loop in this situation.
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
// If there is only 1 Item in the set, we can just assign it to self.
self = ItemsSet.popFirst()!
} else {
// Since all "any_of" JSON arrays are taken care of by the service
// structure, all Items instances in the set are decoded from an
// unkeyed JSON array.
self = .allOfItems(itemsSet)
}
}
func encode(to encoder: Encoder) throws {
// TODO: encode to JSON here
}
case item(NameVersion)
// A set of `Items` instances with an "OR" relationship.
// This represents a JSON array with an "any_of" key.
case anyOfItems(Set<Items>)
// A set of `Item` instances with an "AND" relationship
// This represents a JSON array without an "any_of" key.
case allOfItems(Set<Items>)
}
尽管存在一种 .nestedContainer()
方法用于从非键控容器中获取嵌套键控容器,该容器保存 { "any_of": [] }
JSON 对象的数据,但嵌套容器无法调用 decode(forKey:, from:)
方法解码 JSON.
相反,我按照 this solution 解码嵌套数据,并创建了以下服务结构来解码 { "any_of": [] }
JSON 对象。
struct AnyOfItems: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
// The decoding part for AnyOfItems is largely the same as that for
// Items, but they differ in that for AnyOfItems, the set of Items
// are given to the .anyOfItems case.
itsms = Items.anyOfItems(itemsSet)
}
}
let items: Items
}
大部分重复代码可以提取到自己的函数:
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
// Still has to be a variable, because .popFirst() is a mutating method.
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { self = ItemsSet.popFirst()! }
else { self = .allOfItems(itemsSet) }
}
func encode(to encoder: Encoder) throws {
// TODO: encode to JSON here
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { items = itemsSet.popFirst()! }
else { items = Items.anyOfItems(itemsSet) }
}
let items: Items
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}
Encodable
一致性
编码更简单。
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
// JSON decoded here
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
// JSON decoded here
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
/**
A memberwise initialiser.
*/
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
Codable
一致性
最后,所有东西都放在一起:
indirect enum Items: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
self = ItemsSet.popFirst()!
} else {
self = .allOfItems(itemsSet)
}
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
/**
Initialises an `Items` instance by decoding from the given `decoder`.
- Parameter decoder: The decoder to read data from.
*/
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
items = Items.anyOfItems(itemsSet)
}
}
/**
Encodes an `Items` instance`.
- Parameter encoder: The encoder to encode data to.
*/
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
/**
A memberwise initialiser.
*/
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}