Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Encode a [String: Encodable] dictionary into JSON using JSONEncoder in Swift 4

I am just curious how can I encode a dictionary with String key and Encodable value into JSON.

For example:

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

The keys in this dict are all of type String, but the type of the values vary.

However, all of these types are allowed in JSON.

I am wondering if there is a way to use JSONEncoder in Swift 4 to encode this dict into JSON Data.

I do understand there are other ways without using JSONEncoder to achieve this, but I am just wondering if JSONEncoder is capable of managing this.

The Dictionary do have a func encode(to encoder: Encoder) throws in an extension, but that only applies for constraint Key: Encodable, Key: Hashable, Value: Encodable, whereas for our dict, it needs constraint Key: Encodable, Key: Hashable, Value == Encodable.

Having a struct for this will be sufficient to use JSONEncoder,

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

However, I am interested to know if the it can be done without specifying the concrete type but just the Encodable protocol.

like image 377
ylorn Avatar asked Jul 20 '18 16:07

ylorn


2 Answers

Just figured out a way to achieve this with a wrapper:

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)

And here is the result:

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

I am still not happy with it. If there are any other approaches, I am more than happy to see it.

Thanks!

Edit 1 Moving the wrapper into an extension of 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)

Result:

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

Edit 2: Take customized strategies into account as per @Hamish 's comments

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)
    }
}
like image 123
ylorn Avatar answered Nov 17 '22 11:11

ylorn


You would need a wrapper since with Encodable protocol to know which item is which to be able to encode it easier.

I suggest Use an enum named JSONValue which has 5 to 6 cases for all Int, String, Double, Array, Dictionary cases. then you can write JSONs in a type-safe way.

This link will help too.

This is how I use it:

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

And then make JSONValue: Encodable and write encoding code for each case.

like image 42
farzadshbfn Avatar answered Nov 17 '22 09:11

farzadshbfn