简体   繁体   English

如何使用JSONDecoder解码未知类型的JSON?

[英]How to use JSONDecoder to decode JSON with unknown type?

Here is my scenario: I have a swift WebSocket server and a Javascript client. 这是我的情况:我有一个快速的WebSocket服务器和一个Javascript客户端。 Over this same WebSocket I will be sending various objects that correspond to different Codable types . 通过同一个WebSocket,我将发送对应于不同Codable类型的各种对象。 It is simple enough to decode if the correct type is known. 如果知道正确的类型,则很容易进行解码。 The difficulty for me is to identify which type is being sent from the client. 对我而言,困难在于确定从客户端发送的是哪种类型。 My first thought was to use JSON that looks like the following: 我的第一个想法是使用类似于以下内容的JSON:

{type: "GeoMarker", 
 data: {id: "2",
        latitude: "-97.32432"
        longitude: "14.35436"}
}

This way I would know to decode data using let marker = try decoder.decode(GeoMarker.self) This seems to be straightforward, but for some reason, I just can't figure out how to extract the data object as JSON so that I can decode it using the GeoMarker type. 这样,我就知道可以使用let marker = try decoder.decode(GeoMarker.self)来解码data这似乎很简单,但是由于某些原因,我只是不知道如何将data对象提取为JSON,因此可以使用GeoMarker类型对其进行解码。

Another solution that I came up with was to create an intermediate type like so: 我想到的另一个解决方案是创建一个中间类型,如下所示:

struct Communication: Decodable {
let message: String?
let markers: [GeoMarker]?
let layers: [GeoLayer]?
}

This way I would could send JSON with the following format: 这样,我可以使用以下格式发送JSON:

{message: "This is a message",
 markers: [{id: "2",
           latitude: "-97.32432"
           longitude: "14.35436"},
           {id: "3",
           latitude: "-67.32432"
           longitude: "71.35436"}]
}

and use let com = try decoder.decode(Communication.self) and unwrap the optional message, markers, and layers variables. 并使用let com = try decoder.decode(Communication.self)并解包可选的消息,标记和图层变量。 This works, but seems clunky, especially if I need more types. 这可行,但似乎很笨拙,尤其是当我需要更多类型时。 I will likely end up needing 8-10 after all is said and done. 说完并完成之后,我可能最终需要8-10。

I have thought through this, but don't feel like I have come up with a satisfactory solution. 我已经考虑了这一点,但是觉得自己没有一个令人满意的解决方案。 Would there be better approaches? 会有更好的方法吗? Is there a standard for this kind of thing that I am unaware of? 我没有意识到这种事情的标准吗?

----EDIT---- - - 编辑 - -

As a natural follow up, how would you go about encoding to that same JSON format, given the same circumstances above? 自然地跟进,在上述相同情况下,您将如何编码为相同的JSON格式?

As your first option you can achieve it by custom decoding. 作为您的第一个选择,您可以通过自定义解码来实现。

First create an enum with associated values for all of your possible data types. 首先,为所有可能的数据类型创建一个具有关联值的枚举。

struct GeoMarker: Decodable {
    let id:Int
    let latitude:Double
    let longitude:Double
}

enum ResponseData {
    case geoMarker(GeoMarker)
    case none
}

Now provide custom decoding for your enum to parse all different types of your data objects. 现在为您的枚举提供自定义解码,以解析所有不同类型的数据对象。

extension ResponseData: Decodable{
    enum CodingKeys: String, CodingKey {
        case type = "type"
        case data = "data"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        switch type {
        case "GeoMarker":
            let data = try container.decode(GeoMarker.self, forKey: .data)
            self = .geoMarker(data)
        default:
            self = .none
        }
    }
}

You can use it something like this... 您可以像这样使用它...

let json = """
{
    "type": "GeoMarker",
    "data": {
        "id": 2,
        "latitude": -97.32432,
        "longitude": 14.35436
    }
}
"""

let testRes = try? JSONDecoder().decode(ResponseData.self, from: json.data(using: .utf8)!)

if let testRes = testRes {
    if case let ResponseData.geoMarker(geoMarker) = testRes {
        print("\(geoMarker.id) \(geoMarker.latitude)  \(geoMarker.longitude)")
    }
}

To implement a custom encoder, use the following. 要实现自定义编码器,请使用以下内容。

extension ResponseData: Encodable{

func encode(to encoder: Encoder) throws {

    let container = encoder.container(keyedBy: CodingKeys.self)

    if case let .geoMarker(geoMarker) = self {
         try container.encode("Marker", forKey: .type)
         try container.encode(geoMarker, forKey: .data)
    }
}

You can use it like this: 您可以像这样使用它:

let marker = GeoMarker(id:2, latitude: "-97.32432", longitude: "14.35436")
let encoder = JSONEncoder()
let data = try encoder.encode(.geoMarker(marker))
//Send data over WebSocket

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM