0
votes

I have a JSON file that can look like this:

{
    "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" }
            ]
        }
    ]
}

The JSON file has a recursive structure. The any_of key indicates an OR relationship between all elements in its array, and the lack of any_of key indicates an AND relationship. I want to decode (and encode) the JSON file using Swift's Codable protocol, and currently I have a Codable struct that represents the name version JSON object:

struct NameVersion: Codable {
    let name: String?
    let version: String?

    func toString() -> String { "\(name ?? "")\(version ?? "")" }
}

and a Codable enum that represents the entire JSON structure:

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 { $0.toString() }.joined(separator: " ∨ ")))"
        case let .allOfItems(items):
            return "(\(items.map { $0.toString() }.joined(separator: " ∧ ")))"
        }
    }
}

I'm having trouble implementing the init(from:) and encode(to:) functions for the Items enum. I checked the Stack Overflow question Swift Codable protocol with recursive enums, but my situation differs from it that my enum isn't nested in a struct, and my item's associated value of NameVersion type does not come directly from a key-value pair.

1
this is not a proper json... try to complete your jsonMohammad Reza Koohkan
@MohamadRezaKoohakn I checked my json against the json standard, and don't see how's it's not a proper json. Could you point out where the errors in my json are? Thanks.Wowbagger and his liquid lunch
Likely improper JSON means no open/close {} since this is an object. Does this tool help you get going at all? app.quicktype.ioBrian
A JSON begins and ends either with {...} or [...]RobOhRob
@Brian thanks for the edit. I tried app.quicktype.io, and although it understands the recursion from a JSON schema, it's not able to produce concise code that conforms to the Codable protocol. I think I found a solution using UnkeyedDecodingContainer, and I'm testing it right now.Wowbagger and his liquid lunch

1 Answers

5
votes

After trying practically everything, I found that the best approach to decode the JSON is through the UnkeyedDecodingContainer protocol.

According to the documentation, an unkeyed container "is used to hold the encoded properties of a decodable type sequentially, without keys." This describes a perfect match for the given JSON structure.

Since Codable is just an alias for Decodable and Encodable, let's make Items conform to Decodable first before Encodable.


Decodable Conformance

Given this Codable structure that holds bottom-level JSON objects:

struct NameVersion: Codable {
    let name: String?
    let version: String?
}

To decode the 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>)
}

Although there exists a .nestedContainer() method for getting a nested keyed container from within the unkeyed container, that holds the data of an { "any_of": [] } JSON object, the nested container is unable to call the decode(forKey:, from:) method to decode JSON.

Instead, I followed this solution for decoding nested data, and created the following service struct for decoding { "any_of": [] } JSON objects.

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
}

The majority of the repeated code can be extracted to its own function:

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 Conformance

Encoding is much simpler.

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 Conformance

Finally, with everything put together:

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
}