I need to decode a JSON structure which might be present in different versions. The decoding is based on the version – different key names, different structures or objects present within.
The source data is assumed to be a dictionary which might contain a value with a version number according to which the further decoding should happen. If the top level dictionary does not have the version key, then some – very likely the most recent version is assumed and used.
The way that I am doing it now is using sparsely documented protocol DecodableWithConfiguration. The documentation says that it is _"used for types that require additional static information". I might be abusing it, since I set a value within the config while decoding (see below). Not sure what the consequences might be.
Here follows a minimal example where just one value of an item is version-dependent.
The top level container with optional version
key:
struct ThingContainer: DecodableWithConfiguration {
public class DecodingConfiguration {
var version: String
init(version: String) {
self.version = version
}
}
let things: [Thing]
enum CodingKeys: String, CodingKey {
case version
case things
}
public init(from decoder: any Decoder, configuration: DecodingConfiguration) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let version = try container.decodeIfPresent(String.self, forKey: .version) {
configuration.version = version
}
things = try container.decode([Thing].self, forKey: .things, configuration: configuration)
}
}
The item that is version dependent, in the (fictional) past the value was encoded as value
, current version is using number
:
struct Thing: DecodableWithConfiguration {
typealias DecodingConfiguration = ThingContainer.DecodingConfiguration
let value: Int
enum CodingKeys: String, CodingKey {
case obsoleteValue = "value"
case value = "number"
}
public init(from decoder: any Decoder, configuration: DecodingConfiguration) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch configuration.version {
case "0": // old version
value = try container.decode(Int.self, forKey: .obsoleteValue)
default: // current and default
value = try container.decode(Int.self, forKey: .value)
}
}
}
Example use:
let config = ThingContainer.DecodingConfiguration(version: "0")
let decoder = JSONDecoder()
let oldVersion = """
{
"version": "0",
"things": [ {"value": 10, "number": 20} ]
}
"""
let oldData = oldVersion.data(using: .utf8)!
let oldContainer = try decoder.decode(ThingContainer.self, from: oldData, configuration: config)
print("Old version value: \(oldContainer.things[0].value)")
let newVersion = """
{
"version": "1",
"things": [ {"value": 10, "number": 20} ]
}
"""
let newData = newVersion.data(using: .utf8)!
let newContainer = try decoder.decode(ThingContainer.self, from: newData, configuration: config)
print("New version value: \(newContainer.things[0].value)")
I am currently using the above, however, I wonder whether it is the designated way? If no, what are the potential issues with the above code? What would be some other Swift-way to do it?