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)