JBlake
JBlake

Reputation: 1064

Swift Codable - how to encode and decode stringified JSON values?

The server I'm talking to expects a message in the format:

{
  "command": "subscribe",
  "identifier": "{\"channel\": \"UserChannel\"}",
  "data": "{\"key\": \"value\"}"
}

Where the identifier and data values is an escaped json string.

I have this so far:

struct ActionCableMessage<Message: Encodable>: Encodable {
    let command: Command
    let identifier: CableChannel
    let data: Message?

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(command, forKey: .command)

        try container.encode(identifier, forKey: .identifier) // ????
    }

    private enum CodingKeys: String, CodingKey {
        case command, identifier, data
    }
}

But I don't know what to do from here. I think I need a protocol that CableChannel and Message can conform to, with a provided extension func that implements encode (to encoder: Encoder), which ensures Encoder must be a JSONEncoder, and if so, uses that to rewrite it's own value as a escaped json string.

I also need to decode this back to the ActionCableMessage struct, but I haven't gotten that far yet.

Upvotes: 4

Views: 1066

Answers (1)

Sweeper
Sweeper

Reputation: 273540

I think I need a protocol that CableChannel and Message can conform to

Well, that protocol is Encodable (or Codable if you prefer).

// just implement these as you normally would
extension CableChannel : Encodable { ... }
extension Message : Encodable { ... }

Then in ActionCableMessage, you use another encoder to encode the inner objects to JSON data, then convert that to string, then encode that string:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(command, forKey: .command)

    let subencoder = JSONEncoder()
    let identifierString = try String(data: subencoder.encode(identifier), encoding: .utf8)
    
    try container.encode(identifierString, forKey: .identifier)

    // do the same for "data"
}

Similarly for decoding:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    command = try container.decode(String.self, forKey: .command)
    let identifierJSONString = try container.decode(String.self, forKey: .identifier)
    // do the same for "data"
    
    let subdecoder = JSONDecoder()
    identifier = try subdecoder.decode(CableChannel.self, from: identifierJSONString.data(using: .utf8)!)
    // do the same for "data"
}

Upvotes: 2

Related Questions