I\'ve got some JSON messages coming in over a websocket connection.
// sample message
{
type: \"person\",
data: {
name: \"john\"
}
}
// some other
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)
}