Как использовать JSONDecoder для декодирования JSON неизвестного типа?

Вот мой сценарий: у меня быстрый сервер WebSocket и клиент Javascript. Через этот же WebSocket я буду отправлять различные объекты, соответствующие разным кодируемым типам. Его достаточно просто расшифровать, если известен правильный тип. Трудность для меня состоит в том, чтобы определить, какой тип отправляется от клиента. Моей первой мыслью было использовать JSON, который выглядит следующим образом:

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

Таким образом, я бы знал, как декодировать data с помощью let marker = try decoder.decode(GeoMarker.self). Это кажется простым, но по какой-то причине я просто не могу понять, как извлечь объект data как JSON, чтобы я мог декодировать его, используя тип GeoMarker.

Другое решение, которое я придумал, состояло в том, чтобы создать промежуточный тип следующим образом:

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

Таким образом, я мог бы отправить JSON в следующем формате:

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

и используйте let com = try decoder.decode(Communication.self) и разверните необязательное сообщение, маркеры и переменные слоев. Это работает, но кажется неуклюжим, особенно если мне нужно больше типов. Мне, вероятно, в конечном итоге понадобится 8-10 после того, как все сказано и сделано.

Я думал об этом, но не чувствую, что нашел удовлетворительное решение. Будут ли лучшие подходы? Есть ли стандарт для такого рода вещей, о котором я не знаю?

----РЕДАКТИРОВАТЬ----

В качестве естественного продолжения, как бы вы поступили с кодированием в тот же формат JSON, учитывая те же обстоятельства, что и выше?


person Joshua Goossen    schedule 19.10.2018    source источник
comment
Непонятно в чем проблема. Трудности с использованием JSONDecoder, когда вы не знаете типы/структуру JSON, уже подробно обсуждались здесь, так что в этом смысле это дубликат. И вы не показали образцы JSON, которые доставляют вам проблемы, поэтому вы не предоставляете MCVE, и помочь невозможно, кроме как в бессмысленных обобщениях. Когда все сказано и сделано, когда вы не знаете, каким будет JSON, JSONDecoder не для вас.   -  person matt    schedule 19.10.2018
comment
Я бы рекомендовал первый подход. JSON с сервера должен иметь 2 ключа: type, который является строкой, и data, который может быть любым. То, как вы декодируете data, зависит от type.   -  person Code Different    schedule 19.10.2018


Ответы (1)


В качестве первого варианта вы можете добиться этого с помощью пользовательского декодирования.

Сначала создайте перечисление со связанными значениями для всех ваших возможных типов данных.

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

enum ResponseData {
    case geoMarker(GeoMarker)
    case none
}

Теперь предоставьте собственное декодирование для вашего перечисления, чтобы анализировать все различные типы ваших объектов данных.

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
        }
    }
}

Вы можете использовать это как-то так...

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)")
    }
}

Чтобы реализовать собственный кодировщик, используйте следующее.

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)
    }
}

Вы можете использовать его следующим образом:

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
person Bilal    schedule 19.10.2018
comment
Потрясающе, спасибо! Когда я работал над этим, естественно, следующим вопросом был Как кодировать? Но вы поставили меня на правильный путь, и я понял, поэтому я расширил свой вопрос и ваш ответ. - person Joshua Goossen; 21.10.2018