3

Here is my scenario: I have a swift WebSocket server and a Javascript client. Over this same WebSocket I will be sending various objects that correspond to different Codable types. It is simple enough to decode if the correct type is known. The difficulty for me is to identify which type is being sent from the client. My first thought was to use JSON that looks like the following:

{type: "GeoMarker", 
 data: {id: "2",
        latitude: "-97.32432"
        longitude: "14.35436"}
}

This way I would know to decode data using let marker = try decoder.decode(GeoMarker.self) This seems to be straightforward, but for some reason, I just can't figure out how to extract the data object as JSON so that I can decode it using the GeoMarker type.

Another solution that I came up with was to create an intermediate type like so:

struct Communication: Decodable {
let message: String?
let markers: [GeoMarker]?
let layers: [GeoLayer]?
}

This way I would could send JSON with the following format:

{message: "This is a message",
 markers: [{id: "2",
           latitude: "-97.32432"
           longitude: "14.35436"},
           {id: "3",
           latitude: "-67.32432"
           longitude: "71.35436"}]
}

and use let com = try decoder.decode(Communication.self) and unwrap the optional message, markers, and layers variables. This works, but seems clunky, especially if I need more types. I will likely end up needing 8-10 after all is said and done.

I have thought through this, but don't feel like I have come up with a satisfactory solution. Would there be better approaches? Is there a standard for this kind of thing that I am unaware of?

----EDIT----

As a natural follow up, how would you go about encoding to that same JSON format, given the same circumstances above?

Joshua Goossen
  • 1,714
  • 1
  • 17
  • 36
  • Unclear what the problem is. The difficulty with using JSONDecoder when you don't know the types / structure of the JSON has been heavily discussed here already, so to that extent this is a duplicate. And you have not shown samples of the JSON that's giving you trouble, so you're not providing an MCVE and it's impossible to help except in pointless generalities. When all is said and done, when you don't know what the JSON will be, JSONDecoder is not for you. – matt Oct 19 '18 at 16:56
  • I would recommend the first approach. The JSON from the server should have 2 keys: `type` which is a string and `data` which can be anything. How you decode `data` depends on `type`. – Code Different Oct 19 '18 at 16:58

1 Answers1

5

As your first option you can achieve it by custom decoding.

First create an enum with associated values for all of your possible data types.

struct GeoMarker: Decodable {
    let id:Int
    let latitude:Double
    let longitude:Double
}

enum ResponseData {
    case geoMarker(GeoMarker)
    case none
}

Now provide custom decoding for your enum to parse all different types of your data objects.

extension ResponseData: Decodable{
    enum CodingKeys: String, CodingKey {
        case type = "type"
        case data = "data"
    }

    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 "GeoMarker":
            let data = try container.decode(GeoMarker.self, forKey: .data)
            self = .geoMarker(data)
        default:
            self = .none
        }
    }
}

You can use it something like this...

let json = """
{
    "type": "GeoMarker",
    "data": {
        "id": 2,
        "latitude": -97.32432,
        "longitude": 14.35436
    }
}
"""

let testRes = try? JSONDecoder().decode(ResponseData.self, from: json.data(using: .utf8)!)

if let testRes = testRes {
    if case let ResponseData.geoMarker(geoMarker) = testRes {
        print("\(geoMarker.id) \(geoMarker.latitude)  \(geoMarker.longitude)")
    }
}

To implement a custom encoder, use the following.

extension ResponseData: Encodable{

func encode(to encoder: Encoder) throws {

    let container = encoder.container(keyedBy: CodingKeys.self)

    if case let .geoMarker(geoMarker) = self {
         try container.encode("Marker", forKey: .type)
         try container.encode(geoMarker, forKey: .data)
    }
}

You can use it like this:

let marker = GeoMarker(id:2, latitude: "-97.32432", longitude: "14.35436")
let encoder = JSONEncoder()
let data = try encoder.encode(.geoMarker(marker))
//Send data over WebSocket
Joshua Goossen
  • 1,714
  • 1
  • 17
  • 36
Bilal
  • 18,478
  • 8
  • 57
  • 72
  • Awesome, thanks! As I worked on this, naturally the next question was "How to encode?" But you got me on the right track and I got it, so I expanded my question and your answer. – Joshua Goossen Oct 21 '18 at 20:53