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 问题 ,但我的情况与它不同,我的枚举没有嵌套在结构中,并且我的 itemNameVersion 类型的关联值没有出现直接来自键值对。

几乎尝试了所有方法后,我发现解码 JSON 的最佳方法是通过 UnkeyedDecodingContainer protocol.

根据文档,未加密的容器 "is used to hold the encoded properties of a decodable type sequentially, without keys." 这描述了给定 JSON 结构的完美匹配。

因为Codable只是DecodableEncodable的别名,所以让ItemsEncodable之前先符合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
}