0

Problem basically comes down to this. My app is receiving messages in this JSON format:

{
    "action": "ready",
    "data": null
}

or

{
    "action": "error",
    "data": {
        "code": String,
        "exception": String,
        "status": Int
    }
}

or

{
    "action": "messageRequest",
    "data": {
        "recipientUserId": String,
        "platform": String,
        "content": String
    }
}

or

{
    "action": "tabsChange",
    "data": {
        "roomsTabs": [{
            "configuration": {
                "accessToken": STRING,
                "url": STRING,
                "userId": STRING
            },
            "id": STRING,
            "isOnline": BOOLEAN,
            "isUnread": BOOLEAN,
            "lastActive": NUMBER,
            "name": STRING,
            "participantBanned": BOOLEAN,
            "platform": STRING,
            "secondParticipant": {
                "id": STRING,
                "platform": STRING,
                "userId": STRING
            },
            "secondParticipantId": STRING,
            "state": STRING,
            "unreadMessages": NUMBER
        ]}
    }
}

As you can see the data object has different structure depending on a message and it can get large (and there's more than 10 of them). I don't want to parse everything by hand, field-by-field, ideal solution would of course be:

struct ChatJsCommand: Codable {
    let action: String
    let data: Any?
}
self.jsonDecoder.decode(ChatJsCommand.self, from: jsonData))

Of course because of Any this can't conform to Codable. I can of course extract manually only the action field, create a map of actions (enum) to struct types and then do JSONDecoder().decode(self.commandMap[ActionKey], data: jsonData). This solution would probably also require some casting to proper struct types to use the objects after parsing. But maybe someone has a better approach? So the class isn't 300 lines? Any ideas greatly appreciated.

Thunder
  • 611
  • 1
  • 8
  • 25
  • No, there's like 10 messages only but would love to avoid a blob struct for parsing, nevertheless thanks for suggestion. – Thunder Feb 14 '19 at 23:27
  • 1
    Basically you just use a custom init so that you can decode `action` and use its value to tell you what struct to use to decode `data`. See the linked question and answers. – matt Feb 14 '19 at 23:29
  • @matt I am not sure it's an exact duplicate. It's not about unknown keys, therefore you don't need customized `CodingKeys` here. – Sulthan Feb 14 '19 at 23:34
  • 1
    @Sulthan You can undupe if you like, that's you're prerogative; I won't be offended. But it does seem to me that the question "how do I proceed differently on the second key based on the value of the first key" is completely answered on SO already. – matt Feb 15 '19 at 00:01
  • https://stackoverflow.com/a/54020428/6630644 – SPatel Feb 15 '19 at 04:58
  • https://stackoverflow.com/a/50674899/6630644 – SPatel Feb 15 '19 at 05:00

1 Answers1

3

Let's start by defining a protocol for data, it can be an empty protocol. That will help us later:

protocol MessageData: Decodable {
}

Now let's prepare our data objects:

struct ErrorData: MessageData {
   let code: String
   let exception: String
   let status: Int
}

struct MessageRequestData: MessageData {
   let recipientUserId: String
   let platform: String
   let content: String
}

(use optionals where needed)

Now, we also have to know the data types:

enum ActionType: String {
   case ready
   case error
   case messageRequest
}

And now the hard part:

struct Response: Decodable {
   let action: ActionType
   let data: MessageData?

   private enum CodingKeys: String, CodingKey {
       case action
       case data
   }

   public init(from decoder: Decoder) throws {
      let values = try decoder.container(keyedBy: CodingKeys.self)
      let action = try values.decode(ActionType.self, forKey: .action)

      self.action = action

      switch action {
         case .ready:
             data = nil
         case .messageRequest:
             data = try values.decode(MessageRequestData.self, forKey: .data)
         case .error:
             data = try values.decode(ErrorData.self, forKey: .data)                 
      }
   }
}

The only trick is to decode action first and then parse depending on the value inside. The only problem is that when using Response, you always have to check action first and then cast data to the type you need. That could be alleviated by merging action and data into one enumeration with associated objects, e.g. :

enum Action {
   case ready
   case error(ErrorData)
   case messageRequest(MessageRequestData)
   case unknown
}

struct Response: Decodable {           
   let action: Action

   private enum CodingKeys: String, CodingKey {
      case action
      case data
   }

   public init(from decoder: Decoder) throws {
       let values = try decoder.container(keyedBy: CodingKeys.self)
       let actionString = try values.decode(String.self, forKey: .action)

       switch actionString {
          case "ready":
             action = .ready
          case "messageRequest":
             let data = try values.decode(MessageRequestData.self, forKey: .data)
             action = .messageRequest(data)
          case "error":
             let data = try values.decode(ErrorData.self, forKey: .data)
             action = .error(data)                 
          default: 
             action = .unknown             
       }
   }
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270