如何使用 Swift Codable 处理部分动态 JSON?
How to handle partially dynamic JSON with Swift Codable?
我有一些 JSON 消息通过 websocket 连接传入。
// sample message
{
type: "person",
data: {
name: "john"
}
}
// some other message
{
type: "location",
data: {
x: 101,
y: 56
}
}
如何使用 Swift 4 和 Codable 协议将这些消息转换为正确的结构?
在 Go 中我可以做类似的事情:"Hey at the moment I only care about the type
field and I'm not interested in the rest (the data
part)."它看起来像这样
type Message struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
如您所见,Data
是 json.RawMessage
类型,稍后可以对其进行解析。这是一个完整的例子 https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal.
我可以在 Swift 中做类似的事情吗?喜欢(还没试过)
struct Message: Codable {
var type: String
var data: [String: Any]
}
然后 switch
在 type
上将字典转换为适当的结构。那行得通吗?
我不会依赖 Dictionary
。我会使用自定义类型。
例如,我们假设:
你知道你要取回哪个对象(因为请求的性质);和
两种类型的响应确实 return 除了 data
.
的内容外结构完全相同
在这种情况下,您可以使用非常简单的通用模式:
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
}
然后,当您想要解析带有 Person
的响应时,它将是:
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)
}
或者解析一个Location
:
do {
let responseObject = try JSONDecoder().decode(ServerResponse<Location>.self, from: data)
let location = responseObject.data
print(location)
} catch let parseError {
print(parseError)
}
人们可以接受更复杂的模式(例如,根据遇到的 type
值动态解析 data
类型),但我不会倾向于追求这种模式,除非必要的。这是一种很好的、简单的方法,可以完成典型的模式,您知道特定请求的关联响应类型。
如果您愿意,可以使用从 data
值解析的内容来验证 type
值。考虑:
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
}
然后,您的 parse
函数不仅可以解析正确的 data
结构,而且可以确认 type
值,例如:
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
}
然后你可以这样称呼它:
do {
let location: Location = try parse(data)
print(location)
} catch let parseError {
print(parseError)
}
这不仅 return 是 Location
对象,而且还在服务器响应中验证 type
的值。我不确定是否值得付出努力,但如果您想这样做,这是一种方法。
如果在处理JSON的时候确实不知道类型,那你只需要写一个init(coder:)
,先解析type
,再解析data
取决于 type
包含的值:
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
}
}
然后您可以执行以下操作:
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)
}
我有一些 JSON 消息通过 websocket 连接传入。
// sample message
{
type: "person",
data: {
name: "john"
}
}
// some other message
{
type: "location",
data: {
x: 101,
y: 56
}
}
如何使用 Swift 4 和 Codable 协议将这些消息转换为正确的结构?
在 Go 中我可以做类似的事情:"Hey at the moment I only care about the type
field and I'm not interested in the rest (the data
part)."它看起来像这样
type Message struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
如您所见,Data
是 json.RawMessage
类型,稍后可以对其进行解析。这是一个完整的例子 https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal.
我可以在 Swift 中做类似的事情吗?喜欢(还没试过)
struct Message: Codable {
var type: String
var data: [String: Any]
}
然后 switch
在 type
上将字典转换为适当的结构。那行得通吗?
我不会依赖 Dictionary
。我会使用自定义类型。
例如,我们假设:
你知道你要取回哪个对象(因为请求的性质);和
两种类型的响应确实 return 除了
data
. 的内容外结构完全相同
在这种情况下,您可以使用非常简单的通用模式:
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
}
然后,当您想要解析带有 Person
的响应时,它将是:
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)
}
或者解析一个Location
:
do {
let responseObject = try JSONDecoder().decode(ServerResponse<Location>.self, from: data)
let location = responseObject.data
print(location)
} catch let parseError {
print(parseError)
}
人们可以接受更复杂的模式(例如,根据遇到的 type
值动态解析 data
类型),但我不会倾向于追求这种模式,除非必要的。这是一种很好的、简单的方法,可以完成典型的模式,您知道特定请求的关联响应类型。
如果您愿意,可以使用从 data
值解析的内容来验证 type
值。考虑:
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
}
然后,您的 parse
函数不仅可以解析正确的 data
结构,而且可以确认 type
值,例如:
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
}
然后你可以这样称呼它:
do {
let location: Location = try parse(data)
print(location)
} catch let parseError {
print(parseError)
}
这不仅 return 是 Location
对象,而且还在服务器响应中验证 type
的值。我不确定是否值得付出努力,但如果您想这样做,这是一种方法。
如果在处理JSON的时候确实不知道类型,那你只需要写一个init(coder:)
,先解析type
,再解析data
取决于 type
包含的值:
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
}
}
然后您可以执行以下操作:
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)
}