Закодируйте словарь [String: Encodable] в JSON, используя JSONEncoder в Swift 4.

Мне просто любопытно, как я могу закодировать словарь с ключом String и значением Encodable в JSON.

Например:

let dict: [String: Encodable] = [
    "Int": 1,
    "Double": 3.14,
    "Bool": false,
    "String": "test"
]

Все ключи в этом dict относятся к типу String, но типы значений различаются.

Однако все эти типы разрешены в JSON.

Мне интересно, есть ли способ использовать JSONEncoder в Swift 4 для кодирования этого dict в JSON Data.

Я понимаю, что есть другие способы добиться этого без использования JSONEncoder, но мне просто интересно, способен ли JSONEncoder справиться с этим.

У Dictionary есть func encode(to encoder: Encoder) throws в расширении, но это применимо только для ограничения Key: Encodable, Key: Hashable, Value: Encodable, тогда как для нашего dict требуется ограничение Key: Encodable, Key: Hashable, Value == Encodable.

Иметь struct для этого будет достаточно, чтобы использовать JSONEncoder,

struct Test: Encodable {
    let int = 1
    let double = 3.14
    let bool = false
    let string = "test"
}

Однако мне интересно узнать, можно ли это сделать без указания конкретного типа, а только с протоколом Encodable.


person ylorn    schedule 20.07.2018    source источник
comment
Вы не можете этого сделать, Codable сильно зависит от конкретных типов. encode( – это универсальный метод, который ожидает конкретный тип, соответствующий Encodable, а не самому протоколу.   -  person vadian    schedule 20.07.2018


Ответы (2)


Просто придумал способ добиться этого с помощью обертки:

struct EncodableWrapper: Encodable {
    let wrapped: Encodable

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

let dict: [String: Encodable] = [
    "Int": 1,
    "Double": 3.14,
    "Bool": false,
    "String": "test"
]
let wrappedDict = dict.mapValues(EncodableWrapper.init(wrapped:))
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try! jsonEncoder.encode(wrappedDict)
let json = String(decoding: jsonData, as: UTF8.self)
print(json)

И вот результат:

{ "Double": 3.1400000000000001, "String": "test", "Bool": false, "Int": 1}

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

Спасибо!

Редактировать 1 Перемещение оболочки в расширение JSONEncoder:

extension JSONEncoder {
    private struct EncodableWrapper: Encodable {
        let wrapped: Encodable

        func encode(to encoder: Encoder) throws {
            try self.wrapped.encode(to: encoder)
        }
    }
    func encode<Key: Encodable>(_ dictionary: [Key: Encodable]) throws -> Data {
        let wrappedDict = dictionary.mapValues(EncodableWrapper.init(wrapped:))
        return try self.encode(wrappedDict)
    }
}

let dict: [String: Encodable] = [
    "Int": 1,
    "Double": 3.14,
    "Bool": false,
    "String": "test"
]

let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try! jsonEncoder.encode(dict)
let json = String(decoding: jsonData, as: UTF8.self)
print(json)

Результат:

{ «Int»: 1, «Double»: 3.1400000000000001, «Bool»: false, «String»: «test» }

Редактировать 2: учитывать индивидуальные стратегии в соответствии с комментариями @Hamish.

private extension Encodable {
    func encode(to container: inout SingleValueEncodingContainer) throws {
        try container.encode(self)
    }
}

extension JSONEncoder {
    private struct EncodableWrapper: Encodable {
        let wrapped: Encodable

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try self.wrapped.encode(to: &container)
        }
    }

    func encode<Key: Encodable>(_ dictionary: [Key: Encodable]) throws -> Data {
        let wrappedDict = dictionary.mapValues(EncodableWrapper.init(wrapped:))
        return try self.encode(wrappedDict)
    }
}
person ylorn    schedule 20.07.2018
comment
Обратите внимание, что прямой вызов обернутого значения encode(to:) будет обходить любые стратегии кодирования для этого типа — см. a> для реализации оболочки, учитывающей стратегии кодирования. - person Hamish; 20.07.2018
comment
@Hamish Хороший улов, я никогда не думал о индивидуальных стратегиях. Кстати, вам нравится общий подход к инициализатору. - person ylorn; 20.07.2018
comment
@Hamish Я пытался использовать ссылку, которую вы разместили, кажется, что singleValueContainer все еще сильно зависит от универсальных типов, и он не будет принимать тип протокола в качестве входных данных. - person ylorn; 20.07.2018
comment
Оболочка AnyEncodable, имеющая общий init<Value : Encodable>(_ value: Value), действительно не будет работать с произвольными значениями Encodable, однако первая оболочка (с init(_ value: Encodable)) будет работать с такими значениями :) - person Hamish; 20.07.2018

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

Я предлагаю использовать перечисление с именем JSONValue, которое имеет от 5 до 6 случаев для всех случаев Int, String, Double, Array, Dictionary. тогда вы можете писать JSON безопасным способом.

Эта ссылка тоже поможет.

Вот как я его использую:

indirect enum JSONValue {
    case string(String)
    case int(Int)
    case double(Double)
    case bool(Bool)
    case object([String: JSONValue])
    case array([JSONValue])
    case encoded(Encodable)
}

А потом сделать JSONValue: Encodable и написать код кодировки для каждого случая.

person farzadshbfn    schedule 20.07.2018