Home > Back-end >  Swift codable recursive enum with dynamic keys
Swift codable recursive enum with dynamic keys

Time:12-17

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.

CodePudding user response:

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)
  • Related