Let's say I have a variable clients: [String: Client]
. Client is my parent class where I have a subclass SpecialClient
. Both classes are Codable, and I want to decode from a JSON string into the dictionary. In this dictionary, I want to have both types of clients.
For that, every client in the JSON string has a variable clientType
which defines if it is a SpecialClient
or not. I now need to read this value and add a SpecialClient
or a normal Client
to the dictionary based on it.
Currently, I just have something like this, which obviously just produces Clients in the dictionary. I did some research but could not find a way.
class Object: Codable {
var clients: [String:Client]
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.clients = try container.decode([String : Client].self, forKey: .clients)
}
}
Have a container with clients from Client and SpecialClient class.
For more context: JSON looks like this:
{
"groups": {
"GroupId1": {
"label": "group1",
"id": "GroupId1"
},
"GroupId2": {
"label": "group2",
"id": "GroupId2"
}
},
"clients": {
"ClientId1": {
"clientType": "APP",
"label": "client1"
},
"ClientId2": {
"clientType": "SPECIAL",
"label": "Client2",
"specialValue": 2
}
}
}
EDIT the OP clarified the JSON structure - it is a single heterogenous dictionary rather than an array of multiple homogenous dictionaries - and the initial approach is no longer appropriate. However leaving the answer here as it may prove useful to future searches. Revised answer is at the end.
There's an assumption made in this answer due to the lack of clarity in the question around the JSON structure. I've assumed the structure is:
{
"clients": [
{
"clientId1111": {
"clientType": "BASIC_TYPE",
"label": "Test1"
}
},
//...
{
"clientId1114": {
"clientType": "SPECIAL_TYPE",
"label": "Test4",
"specialCode": 4
}
}
]
}
i.e. an array of dictionaries, keyed at the top level by clients
if this isn't the case you may need to adapt the answer.
On this basis I've created a number of models:
The base Client
model. This complies with Decodable out the box so no custom decoding is required.
class Client: Decodable {
let label: String
let clientType: String
init(label: String, clientType: String) {
self.label = label
self.clientType = clientType
}
}
The SpecialClient
model:
class SpecialClient: Client {
let specialCode: Int //just to differentiate from Client
enum CodingKeys: CodingKey {
case label, clientType, specialCode
}
required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
specialCode = try container.decode(Int.self, forKey: .specialCode)
let label = try container.decode(String.self, forKey: .label)
let type = try container.decode(String.self, forKey: .clientType)
super.init(label: label, clientType: type)
}
}
This uses a custom decoder and calls super.init
with the data fields. [I feel there is a better way to handle this but it's so long since I've used inheritance with coddle...!]
and then the bulk of the decoding happens in the Object
model
class Object: Decodable {
var clients: [String:Client]
enum CodingKeys: CodingKey {
case clients
}
required init(from decoder: any Decoder) throws {
var imports = [ [String: Client] ]()
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
var dataContainer = try container.nestedUnkeyedContainer(forKey: .clients)
while !dataContainer.isAtEnd {
do {
let imported = try dataContainer.decode([String: SpecialClient].self)
imports.append(imported)
} catch {
let imported = try dataContainer.decode([String: Client].self)
imports.append(imported)
}
}
} catch {
fatalError(error.localizedDescription)
}
clients = imports.reduce(into:[String: Client]()){ master, childDict in
childDict.forEach{ childPair in
master[childPair.key] = childPair.value
}
}
}
}
here it uses a nested unkeyed container so you can access both the dictionary key (the clientId###
) and its value (either a Client
or a SpecialClient
).
The method iterates through each entry in the unkeyed container and for each initially tries to decode the more specialised child class, and then if that fails tries to decode the simpler parent class in the catch block. The decoded pair of values, i.e. the [String: Client/SpecialClient]
dictionary, is appended to a temporary array.
Finally it reduced (flattens) the temporary array of dictionaries into the main clients
dictionary.
Working on my test json, object.clients.forEach{print($0)}
produces this output:
(key: "clientId1114", value: __lldb_expr_18.SpecialClient)
(key: "clientId1112", value: __lldb_expr_18.Client)
(key: "clientId1111", value: __lldb_expr_18.Client)
(key: "clientId1113", value: __lldb_expr_18.SpecialClient)
as expected.
In practice if I was implementing this I'd use structs for the Client types, not classes, and hide them behind a protocol for the dictionary. This would simplify some of the decoding as there's no need to deal with things like super.init methods
To decode a dictionary that has a variety of types it is necessary to find a single structure that can represent all of these as this will allow Decodable
to process the dictionary as a single object. The alternative would be to process it almost line-by-line, probably with JSONSerialization
, which would be heavy going. Better to let Decodable
do the heavy lifting.
The way to approach this is use an enum with associated values, with a case for each of the possible types in the dictionary. This will require a custom init(from:)
. To support the realistic JSON that has now been added to the question, the following approach is a viable solution...
enum AnyClient: Decodable {
case standard(Client)
case special(SpecialClient)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let client = try container.decode(SpecialClient.self)
self = .special(client)
} catch DecodingError.keyNotFound {
let client = try container.decode(Client.self)
self = .standard(client)
}
}
}
Once you have this in place, the rest of the decoding becomes standard Dedcodable
although with the slight overhead of supporting the inheritance in the Client
/ SpecialClient
classes
class Client: Decodable {
let label: String
let clientType: String
init(label: String, clientType: String) {
self.label = label
self.clientType = clientType
}
}
class SpecialClient: Client {
let specialValue: Int //just to differentiate from Client
enum CodingKeys: CodingKey {
case label, clientType, specialValue
}
required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
specialValue = try container.decode(Int.self, forKey: .specialValue)
let label = try container.decode(String.self, forKey: .label)
let type = try container.decode(String.self, forKey: .clientType)
super.init(label: label, clientType: type)
}
}
class Group: Decodable {
let label: String
let id: String
}
class Object: Decodable {
var groups: [String: Group]
var clients: [String: AnyClient]
}
At this point you have decoded the json. The client dictionary values are now wrapped inside an enum; this may be helpful or a hindrance moving forwards depending how you are planning to use them elsewhere, but it would be an easy task to iterate through the dictionary transform it to [String: Client]
. This could even be done inside a custom .init(from:)
in Object
. Susch as
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.groups = try container.decode([String : Group].self, forKey: .groups)
let anyClients = try container.decode([String : AnyClient].self, forKey: .clients)
clients = anyClients.reduce(into: [String: Client]()) { dict, item in
switch item.value {
case let .standard(client): dict[item.key] = client
case let .special(client): dict[item.key] = client
}
}
}
passing the provided json through:
let data = json.data(using: .utf8)!
let object = try JSONDecoder().decode(Object.self, from: data)
object.groups.forEach{print($0)}
object.clients.forEach{print($0)}
provides the expected output of
(key: "GroupId1", value: __lldb_expr_161.Group)
(key: "GroupId2", value: __lldb_expr_161.Group)
(key: "ClientId1", value: __lldb_expr_161.AnyClient.standard(__lldb_expr_161.Client))
(key: "ClientId2", value: __lldb_expr_161.AnyClient.special(__lldb_expr_161.SpecialClient))