swiftcodableparameter-pack

How to write a Codable type that uses type packs?


I am trying to decode some JSON from a server that looks like this:

{
    "data": {
        "fields": ["column1", "column2"],
        "values": [
            ["value 1", { "property": 1 }],
            ["value 2", { "property": 2 }],
        ]
    }
}

Notably, the inner arrays of the values 2D array can contain many different types of objects depending on the request I sent to the server. For context, I am using the HTTP Query API of a Neo4J database. The values 2D array contains the rows returned by the query. The inner arrays contain heterogeneous elements because the columns returned by the query can have different types.

For each request, I always know how many elements the inner arrays are going to contain and the type of each element. Therefore, I thought I should create a Decodable type that has a parameter pack, representing the types of the elements in the array.

public struct NJQueryResponse<each Result: Decodable & Sendable>: Decodable, Sendable {
    public let data: Data
    public struct Data: Decodable, Sendable {
        public let fields: [String]
        public var rows: [(repeat each Result)]
        
        enum CodingKeys: CodingKey {
            case fields, values
        }
        
        public init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            fields = try container.decode([String].self, forKey: .fields)
            rows = []
            var rowsContainer = try container.nestedUnkeyedContainer(forKey: .values)
            while !rowsContainer.isAtEnd {
                var rowContainer = try rowsContainer.nestedUnkeyedContainer()
                let row = (repeat try rowContainer.decode((each Result).self))
                rows.append(row)
            }
        }
    }
}

For the JSON shown above as an example, I could call JSONDecoder.decode with the type NJQueryResponse<String, [String: Int]>.

However, there is an error at the declaration of CodingKeys.

Enums cannot declare a type pack

This makes me confused. CodingKeys does not have a type pack.

How can I fix this error?


Solution

  • This has been reported as a bug on the Swift repo - #72069. The compiler seems to think that the each Result type pack is declared by the CodingKeys enum.

    A workaround is to move the enum outside of NJQueryResponse.

    Note that as part of the Decodable conformance synthesis for the outer NJQueryResponse struct, a CodingKeys enum (for the data key) is also generated. However, because of the compiler bug, this will also not compile. This means that you will need to manually implement Decodable for NJQueryResponse too, not just NJQueryResponse.Data.

    Final code:

    public struct NJQueryResponse<each Result: Decodable & Sendable>: Decodable, Sendable {
        public let data: Data
        public struct Data: Decodable, Sendable {
            public let fields: [String]
            public var rows: [(repeat each Result)]
            
            public init(from decoder: any Decoder) throws {
                let container = try decoder.container(keyedBy: NJQueryResponseDataCodingKeys.self)
                fields = try container.decode([String].self, forKey: .fields)
                rows = []
                var rowsContainer = try container.nestedUnkeyedContainer(forKey: .values)
                while !rowsContainer.isAtEnd {
                    var rowContainer = try rowsContainer.nestedUnkeyedContainer()
                    let row = (repeat try rowContainer.decode((each Result).self))
                    rows.append(row)
                }
            }
        }
        
        public init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: NJQueryResponseCodingKeys.self)
            self.data = try container.decode(NJQueryResponse<repeat each Result>.Data.self, forKey: .data)
        }
    }
    
    private enum NJQueryResponseCodingKeys: CodingKey {
        case data
    }
    
    private enum NJQueryResponseDataCodingKeys: CodingKey {
        case fields, values
    }