18

I've got some JSON messages coming in over a websocket connection.

// sample message
{
  type: "person",
  data: {
    name: "john"
  }
}

// some other message
{
  type: "location",
  data: {
    x: 101,
    y: 56
  }
}

How can I convert those messages into proper structs using Swift 4 and the Codable protocol?

In Go I can do something like: "Hey at the moment I only care about the type field and I'm not interested in the rest (the data part)." It would look like this

type Message struct {
  Type string `json:"type"`
  Data json.RawMessage `json:"data"`
}

As you can see Data is of type json.RawMessage which can be parsed later on. Here is a full example https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal.

Can I do something similar in Swift? Like (haven't tried it yet)

struct Message: Codable {
  var type: String
  var data: [String: Any]
}

Then switch on the type to convert the dictionary into proper structs. Would that work?

zemirco
  • 16,171
  • 8
  • 62
  • 96
  • Check this file: https://github.com/iwheelbuy/VK/blob/master/VK/Core/Object/Attachment/VK_Object_Attachment.swift – iWheelBuy Dec 28 '17 at 12:46
  • FYI, your JSON examples are missing quotation marks around `type`, `data`, `name`, `x` and `y`... – Rob Dec 28 '17 at 13:40

1 Answers1

21

I wouldn't rely upon a Dictionary. I'd use custom types.

For example, let's assume that:

  • you know which object you're going to get back (because of the nature of the request); and

  • the two types of response truly return identical structures except the contents of the data.

In that case, you might use a very simple generic pattern:

struct Person: Decodable {
    let name: String
}

struct Location: Decodable {
    let x: Int
    let y: Int
}

struct ServerResponse<T: Decodable>: Decodable {
    let type: String
    let data: T
}

And then, when you want to parse a response with a Person, it would be:

let data = json.data(using: .utf8)!
do {
    let responseObject = try JSONDecoder().decode(ServerResponse<Person>.self, from: data)

    let person = responseObject.data
    print(person)
} catch let parseError {
    print(parseError)
}

Or to parse a Location:

do {
    let responseObject = try JSONDecoder().decode(ServerResponse<Location>.self, from: data)

    let location = responseObject.data
    print(location)
} catch let parseError {
    print(parseError)
}

There are more complicated patterns one could entertain (e.g. dynamic parsing of the data type based upon the type value it encountered), but I wouldn't be inclined to pursue such patterns unless necessary. This is a nice, simple approach that accomplishes typical pattern where you know the associated response type for a particular request.


If you wanted you could validate the type value with what was parsed from the data value. Consider:

enum PayloadType: String, Decodable {
    case person = "person"
    case location = "location"
}

protocol Payload: Decodable {
    static var payloadType: PayloadType { get }
}

struct Person: Payload {
    let name: String
    static let payloadType = PayloadType.person
}

struct Location: Payload {
    let x: Int
    let y: Int
    static let payloadType = PayloadType.location
}

struct ServerResponse<T: Payload>: Decodable {
    let type: PayloadType
    let data: T
}

Then, your parse function could not only parse the right data structure, but confirm the type value, e.g.:

enum ParseError: Error {
    case wrongPayloadType
}

func parse<T: Payload>(_ data: Data) throws -> T {
    let responseObject = try JSONDecoder().decode(ServerResponse<T>.self, from: data)

    guard responseObject.type == T.payloadType else {
        throw ParseError.wrongPayloadType
    }

    return responseObject.data
}

And then you could call it like so:

do {
    let location: Location = try parse(data)
    print(location)
} catch let parseError {
    print(parseError)
}

That not only returns the Location object, but also validates the value for type in the server response. I'm not sure it's worth the effort, but in case you wanted to do so, that's an approach.


If you really don't know the type when processing the JSON, then you just need to write an init(coder:) that first parses the type, and then parses the data depending upon the value that type contained:

enum PayloadType: String, Decodable {
    case person = "person"
    case location = "location"
}

protocol Payload: Decodable {
    static var payloadType: PayloadType { get }
}

struct Person: Payload {
    let name: String
    static let payloadType = PayloadType.person
}

struct Location: Payload {
    let x: Int
    let y: Int
    static let payloadType = PayloadType.location
}

struct ServerResponse: Decodable {
    let type: PayloadType
    let data: Payload

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        type = try values.decode(PayloadType.self, forKey: .type)
        switch type {
        case .person:
            data = try values.decode(Person.self, forKey: .data)
        case .location:
            data = try values.decode(Location.self, forKey: .data)
        }
    }

    enum CodingKeys: String, CodingKey {
        case type, data
    }

}

And then you can do things like:

do {
    let responseObject = try JSONDecoder().decode(ServerResponse.self, from: data)
    let payload = responseObject.data
    if payload is Location {
        print("location:", payload)
    } else if payload is Person {
        print("person:", payload)
    }
} catch let parseError {
    print(parseError)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Thank you for the detailed response. The problem is that I do not know the type of the incoming message. It is not request/response based but a websocket connection. The messages itself are all equal having `type` and `data` keys. – zemirco Dec 28 '17 at 16:38
  • 1
    Then you will need to write an `init(coder:)` that first parses the `type`, and then parses the `data` as the corresponding type. See expanded answer above. – Rob Dec 28 '17 at 18:32
  • Thanks again! And Happy New Year! It works and I've got a stable websocket communication between ios <-> golang <-> js. However my `payload` in the final `if` clause if always of type `Payload`. Therefore I cannot do something like (in case it is `Location`) `print(payload.x)`. How do I get real types instead of protocols? – zemirco Jan 01 '18 at 13:38
  • `if let person = payload as? Person { ... }` or `if let location = payload as? Location { ... }` – Rob Jan 01 '18 at 14:41
  • Please look into this and give your opinion. https://stackoverflow.com/questions/49181230/how-to-decode-a-nested-json-key-which-holds-dynamic-object-using-codable – Kunal Kumar Mar 09 '18 at 05:05
  • @Rob so how to manage case when "type" is returned everytime, and based on that type next codingKey would be instead of "data" with a generic pattern? i.e. `{ type: "location", location: {x:1, y:1}}` ? – klapinski Mar 20 '18 at 23:10
  • The Best! Thanks for sharing the good implementation case for this situation. It's great! – Tommy Apr 01 '19 at 02:29