jsonswiftdecoding

Decoding Dictionary depending on ClassType


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
    }
  }
}

Solution

  • 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.


    Initial answer for multiple homogenous dictionaries

    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


    Updated answer for single heterogenous dictionary

    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
             }
          }
       }
    

    Example output

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