realtimez
realtimez

Reputation: 2555

How to manually implement Codable for multiple structs according to 'type'?

Consider the following json:

{
    "from": "Guille",
    "text": "Look what I just found!",
    "attachments": [
        {
            "type": "image",
            "payload": {
                "url": "http://via.placeholder.com/640x480",
                "width": 640,
                "height": 480
            }
        },
        {
            "type": "audio",
            "payload": {
                "title": "Never Gonna Give You Up",
                "url": "https://audio.com/NeverGonnaGiveYouUp.mp3",
                "shouldAutoplay": true,
            }
        }
    ]
}

And the following Swift structure:

struct ImageAttachment: Codable {
    let url: URL
    let width: Int
    let height: Int
}
struct AudioAttachment: Codable {
    let title: String
    let url: URL
    let shouldAutoplay: Bool
}

enum Attachment {
  case image(ImageAttachment)
  case audio(AudioAttachment)
  case unsupported
}

extension Attachment: Codable {
  private enum CodingKeys: String, CodingKey {
    case type
    case payload
  }
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let type = try container.decode(String.self, forKey: .type)
    switch type {
    case "image":
      let payload = try container.decode(ImageAttachment.self, forKey: .payload)
      self = .image(payload)
    case "audio":
      let payload = try container.decode(AudioAttachment.self, forKey: .payload)
      self = .audio(payload)
    default:
      self = .unsupported
    }
  }
  ...
}

How would I go about handling the similar use case if the 'payload' key params were flat (aka without 'payload') like:

{
  "type": "image",
  "url": "http://via.placeholder.com/640x480",
  "width": 640,
  "height": 480
}

{
  "type": "audio",
  "title": "Never Gonna Give You Up",
  "url": "https://audio.com/NeverGonnaGiveYouUp.mp3",
  "shouldAutoplay": true,
 }

I can't figure out how to implement the init decoder properly for the flat case since while preserving the Attachment structures and allowing for future flexibility (of adding more types of attachments).

Upvotes: 2

Views: 253

Answers (1)

Code Different
Code Different

Reputation: 93151

You only need to make a small change

extension Attachment: Codable {
    private enum CodingKeys: String, CodingKey {
        case type
        case payload
    }

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

        // The attachment data is nested if it has the `payload` key
        let isNested = container.allKeys.contains(.payload)

        switch type {
        case "image":
            // If the attachment data is nested inside the `payload` property, decode
            // it from that property. Otherwise, decode it from the current decoder
            let payload = try isNested ? container.decode(ImageAttachment.self, forKey: .payload) : ImageAttachment(from: decoder)
            self = .image(payload)
        case "audio":
            // Same as image attachment above
            let payload = try isNested ? container.decode(AudioAttachment.self, forKey: .payload) : AudioAttachment(from: decoder)
            self = .audio(payload)
        default:
            self = .unsupported
        }
    }
}

Upvotes: 1

Related Questions