如何使用 Swift Codable 处理部分动态 JSON?

Posted

技术标签:

【中文标题】如何使用 Swift Codable 处理部分动态 JSON?【英文标题】:How to handle partially dynamic JSON with Swift Codable? 【发布时间】:2018-06-09 00:26:29 【问题描述】:

我收到了一些通过 websocket 连接传入的 JSON 消息。

// sample message

  type: "person",
  data: 
    name: "john"
  


// some other message

  type: "location",
  data: 
    x: 101,
    y: 56
  

如何使用 Swift 4 和 Codable 协议将这些消息转换为适当的结构?

在 Go 中,我可以执行以下操作:“嘿,目前我只关心 type 字段,而我对其余部分不感兴趣(data 部分)。”它看起来像这样

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]

然后在type 上的switch 将字典转换为适当的结构。这行得通吗?

【问题讨论】:

检查这个文件:github.com/iwheelbuy/VK/blob/master/VK/Core/Object/Attachment/… 仅供参考,您的 JSON 示例在 typedatanamexy 周围缺少引号... 【参考方案1】:

我不会依赖Dictionary。我会使用自定义类型。

例如,假设:

您知道要取回哪个对象(因为请求的性质);和

除了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)

这不仅会返回Location 对象,还会验证服务器响应中type 的值。我不确定这是否值得,但如果您想这样做,这是一种方法。


如果你在处理JSON的时候真的不知道类型,那么你只需要写一个init(coder:)先解析type,然后根据type的值解析data包含:

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)

【讨论】:

感谢您的详细回复。问题是我不知道传入消息的类型。它不是基于请求/响应,而是基于 websocket 连接。消息本身都具有typedata 键。 那你需要写一个init(coder:),先解析type,再解析data为对应的类型。请参阅上面的扩展答案。 再次感谢!新年快乐!它可以工作,并且我在 ios golang js 之间建立了稳定的 websocket 通信。但是我的payload 在最后的if 子句中,如果总是Payload 类型。因此我不能做类似的事情(如果是Locationprint(payload.x)。如何获取真实类型而不是协议? if let person = payload as? Person ... if let location = payload as? Location ... 请对此进行调查并提出您的意见。 ***.com/questions/49181230/…

以上是关于如何使用 Swift Codable 处理部分动态 JSON?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Codable 在 Swift 中使用动态文件名解析 JSON

Swift 根据响应类型为 JSON 字段值动态选择 Codable 结构

带有动态键的 Swift Codable

如何使用 Swift 的 Codable 编码成字典?

如何使用 Codable 和 Swift 解析这个嵌套的 JSON?

如何使用 Swift 创建 JSON Codable [关闭]