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

时间:2017-12-28 12:43:55

标签: ios swift codable

我通过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]
}

然后在switchtype将字典转换为正确的结构。那会有用吗?

1 个答案:

答案 0 :(得分:10)

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

可以接受更复杂的模式(例如,基于它遇到的data值动态解析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<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,然后根据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)
}

然后你可以做以下事情:

ListViewDataItem dataItem = e.Item as ListViewDataItem; 
int commentId = (int)DataBinder.Eval(dataItem.DataItem, "CommentId");
Guid authorId = (Guid)DataBinder.Eval(dataItem.DataItem, "UserId");