如何使用 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"`
}

如您所见,Datajson.RawMessage 类型,稍后可以对其进行解析。这是一个完整的例子 https://golang.org/pkg/encoding/json/#example_RawMessage_unmarshal.

我可以在 Swift 中做类似的事情吗?喜欢(还没试过)

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

然后 switchtype 上将字典转换为适当的结构。那行得通吗?

我不会依赖 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)
}