Как кодировать словарь с помощью JSONEncoder в Swift 4

Я хочу кодировать словарь в json с помощью JSONEncoder. Это похоже на запрос, получите словарь как параметр и закодируйте его в json как тело http. Код выглядит так:

let dict = ["name": "abcde"]

protocol Request {
    var params: [String: Encodable] { get set }
    func encode<T>(_ value: T) throws -> Data where T : Encodable
}

extension Request {
    func encode<T>(_ value: T) throws -> Data where T : Encodable {
        return try JSONEncoder().encode(value)
    }

    var body: Data? {
        if let encoded = try? self.encode(self.params) {
            return encoded
        }
        return nil
    }
}

struct BaseRequest: Request {
    var params: [String : Encodable]
}

let req = BaseRequest(params: dict)
let body = req.body

Но в этом коде возникает ошибка

Неустранимая ошибка: Dictionary ‹String, Encodable> не соответствует Encodable, потому что Encodable не соответствует самому себе. Вы должны использовать конкретный тип для кодирования или декодирования.

Как я мог сделать это кодируемым?


person 鸡肉味嘎嘣脆    schedule 31.01.2018    source источник
comment
Почему бы вместо этого не использовать JSONSerializer? Вы хотите предотвратить Any зависимости?   -  person Timofey Solonin    schedule 31.01.2018
comment
Да, мне нужны параметры как [String: Any]   -  person 鸡肉味嘎嘣脆    schedule 31.01.2018
comment
Какие типы могут оказаться значениями в словаре? Это не совсем Any, а один из нескольких известных типов, верно? Часто лучшим решением этого является создание перечисления с этими типами в качестве связанных значений, что подтверждает Encodable.   -  person Itai Ferber    schedule 31.01.2018
comment
Наконец, я добавляю ассоциированный тип в запрос   -  person 鸡肉味嘎嘣脆    schedule 06.02.2018


Ответы (2)


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

struct AnyEncodable: Encodable {

    let value: Encodable
    init(value: Encodable) {
        self.value = value
    }

    func encode(to encoder: Encoder) throws {
        try value.encode(to: encoder)
    }

}

struct Model: Encodable {

    var params: [String: AnyEncodable]

}

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try! encoder.encode(
    Model(
        params: [
            "hello" : AnyEncodable.init(value: "world")
        ]
    ).params
)
print(String(data: json, encoding: .utf8))
person Timofey Solonin    schedule 31.01.2018
comment
К сожалению, такое стирание типа не позволяет кодировщику перехватить тип перед кодированием. Это означает, что если вы пытаетесь кодировать тип, который может иметь стратегию кодирования (например, Dates или Data), она не будет применена. - person Itai Ferber; 31.01.2018
comment
@ItaiFerber AnyEncodable украшает полученное значение Encodable. Он использует реализацию, определенную самим Encodable (который определяется по-разному для каждой конкретной реализации). Я не понимаю, как это нарушает различные стратегии кодирования, если только вы не пытаетесь ввести приведение, что является очень плохой идеей. - person Timofey Solonin; 31.01.2018
comment
Кроме того, если вам нужно прервать инкапсуляцию, вы можете ввести параметр cast value в AnyEncodable. - person Timofey Solonin; 31.01.2018
comment
Когда вы encode(...) значение через один из контейнеров JSONEncoder, оно заканчивается упаковкой значения для кодирования. Поскольку все методы кодирования являются общими, он знает тип того, что кодируется, и может перехватить это, чтобы применить стратегию кодирования. Вы можете увидеть это в реализации box_: он проверяет применимость определенных типов. Однако, когда вы делаете value.encode(to: encoder), вы меняете отношение и вызываете реализацию базового типа напрямую. - person Itai Ferber; 31.01.2018
comment
Например, с Date вы в конечном итоге кодируете дату как Double всегда, поскольку именно так Date кодируется по умолчанию (фактически, код запрашивает дату, пожалуйста, закодируйте себя в кодировщике, а не в кодировщике, пожалуйста, закодируйте эту дату). encoder никогда не видел, чтобы было Date, поскольку Date.encode вызывается напрямую, который просто кодирует значение временного интервала. - person Itai Ferber; 31.01.2018
comment
Это поведение можно увидеть в этой сути: при заключении даты в AnyEncodable теряется контекст типа, поэтому кодировщик не может применить DateEncodingStrategy. Если вы не используете стратегии, то это, конечно, нормально. Это просто предостережение, чтобы вы не столкнулись с конфликтующими форматами значений внутри одной и той же закодированной полезной нагрузки. - person Itai Ferber; 31.01.2018
comment
@ItaiFerber спасибо, что указали на это! Я даже не знал, что такое поведение существует с кодировщиком. Однако изменение кодировщика в любом случае - плохая идея. Почему бы не ввести два концептуальных декоратора, таких как StandardDate и FormattedDate, для печати даты по-разному на кодировщике. Они могут изменить состояние кодировщика в методе encode, применить себя и вернуть кодировщик в исходное состояние. - person Timofey Solonin; 31.01.2018
comment
Обычно существует компромисс между правильностью (т. Е. Не нарушением инкапсуляции) и полезностью. JSON очень часто отправляется на серверы, которые предъявляют строгие требования к форматам дат (поскольку JSON не указывает, как даты должны быть закодированы, каждый сервер отличается), и очень часто вам нужно кодировать типы, которые у вас нет. собственные и не могут повлиять - если эти типы кодируют Dates, а не StandardDate или FormattedDate, вы ничего не можете сделать. Мы предлагаем эти стратегии для очень ограниченного набора типов (пока только Date и Data) из-за этого компромисса. - person Itai Ferber; 01.02.2018
comment
@ItaiFerber Я понимаю ваши рассуждения, но, вводя концепции, нарушающие инкапсуляцию, мы создаем цикл положительной обратной связи, в котором разработчики сокращают углы вместо того, чтобы пытаться разработать удобные концепции объектно-ориентированного программирования, которые решат ограничение строгой инкапсуляции дизайна. - person Timofey Solonin; 01.02.2018

Если вы хотите определить свою структуру как соответствующую Codable, вы можете сделать это следующим образом:

struct Model: Codable {
    var param1: String
    var param2: Int
}

let model = Model(param1: "test", param2: 0)
let encoded = try? JSONEncoder().encode(model)
let decoded = try? JSONDecoder().decode(Model.self, from: encoded!)

Это не будет работать, если вы установите params: [String: Any], потому что кодировщики / декодеры не знают, как кодировать / декодировать Any, но они могут делать это для примитивных типов.

Если вам нужна дополнительная помощь, вы должны узнать больше о новом протоколе Codable. Я рекомендую это: https://hackernoon.com/everything-about-codable-in-swift-4-97d0e18a2999

person Guy Kogus    schedule 31.01.2018
comment
Ответ Тимофея Солонина показывает хороший способ инкапсулировать Encodable Any, если вы хотите его использовать. Однако вам лучше точно определить типы, используемые вашими моделями, чтобы вам не нужно было их инкапсулировать. - person Guy Kogus; 31.01.2018