swiftenumscodabledecodableencodable

Swift codable recursive enum with dynamic keys


I am trying to write a struct for dynamic data. The keys of the data are unknown as are their values. The struct looks like so:

enum EntryData: Codable {
   case string(String)
   case array([EntryData]
   case nested([String: EntryData])
}

struct Entry: Codable {
   var data: [String: EntryData]
}

The goal of this is to be able to decode JSON like this:

{
  "data": {
    "testA": "valueA",
    "testB": ["valueB", ["valueC"]],
    "testC": {
      "testD": "valueD",
      "testE": ["valueE", "valueF"]
    }
  }
}

And having the following code:

var data = EntryData(data: [
    "testA": .string("valueA"),
    "testB": .array([.string("valueB"), .array([.string("valueC")])]),
    "testC": .nested([
        "testD": .string("valueD"),
        "testeE": .array([.string("valueE"), .string("valueF")])
    ])
])

Encode in to the above JSON output.

Is this possible in Swift? If so, how would an implementation look like?

Many thanks in advance.


Solution

  • You can use singleValueContainer to decode/encode the each case of EntryData, without using any hardcoded keys. When decoding, we can try to decode as all three cases, and see which one succeeded.

    enum EntryData: Codable {
       case string(String)
       case array([EntryData])
       case nested([String: EntryData])
        
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let nested = try? container.decode([String: EntryData].self)
            let array = try? container.decode([EntryData].self)
            let string = try? container.decode(String.self)
            switch (string, array, nested) {
            case (let s?, nil, nil):
                self = .string(s)
            case (nil, let a?, nil):
                self = .array(a)
            case (nil, nil, let n?):
                self = .nested(n)
            default:
                throw DecodingError.valueNotFound(
                    EntryData.self, 
                    .init(codingPath: decoder.codingPath, 
                          debugDescription: "Value must be either string, array or a dictionary!", 
                          underlyingError: nil))
            }
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .string(let s):
                try container.encode(s)
            case .array(let a):
                try container.encode(a)
            case .nested(let n):
                try container.encode(n)
            }
        }
    }
    

    Now you can do:

    let entry = try JSONDecoder().decode(Entry.self, from: data)