2
votes

I'm looking to store models objects in a Dictionary and would like to serialize the whole dictionary using JSONEncoder into data and subsequently into a string and save it.

The idea is to use Swift 4's out of the box Encodable to ensure anything that I add to the dictionary will be serialized which can include primitives and custom objects (which will themselves conform to Encodable).

The Challenge is what type should I declare the dictionary to be:

  • If I use [String: Any], it won't know how to encode Any, and if I have to cast it into an actual concrete type, it kind of defeats the purpose of generics
  • If I use [String: Encodable], it will crash at run time saying Encodable doesn't conform to itself, which is understandable as it needs a concrete type

In order to tackle this, I thought of creating a wrapper: i.e A protocol with an associated type or a struct with generic type value:

struct Serializable<T: Encodable> {
    var value: T?

    init(value: T) {
       self.value = value
    }
}

But the problem remains, while declaring the type of the aforementioned dictionary, I still have to supply the concrete type..

var dictionary: [String: Serializable<X>]

What should 'X' be here, Or, what's the correct way to achieve this? What am I missing?

2

2 Answers

2
votes

Two possible approaches:

  1. You can create dictionary whose values are Encodable wrapper type that simply encodes the underlying value:

    struct EncodableValue: Encodable {
        let value: Encodable
    
        func encode(to encoder: Encoder) throws {
            try value.encode(to: encoder)
        }
    }
    

    Then you can do:

    let dictionary = [
        "foo": EncodableValue(value: Foo(string: "Hello, world!")),
        "bar": EncodableValue(value: Bar(value: 42)),
        "baz": EncodableValue(value: "qux")
    ]
    
    let data = try! JSONEncoder().encode(dictionary)
    
  2. You can define your own Codable type instead of using dictionary:

    struct RequestObject: Encodable {
        let foo: Foo
        let bar: Bar
        let baz: String
    }
    
    let requestObject = RequestObject(
        foo: Foo(string: "Hello, world!"), 
        bar: Bar(value: 42),
        baz: "qux"
    )
    
    let data = try! JSONEncoder().encode(requestObject)
    

Needless to say, these both assume that both Foo and Bar conform to Encodable.

0
votes

This is my solution (improved by Rob answer):

struct EncodableValue: Encodable {
    let value: Encodable

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

struct Storage: Encodable {
    var dict: [String: Encodable] = [:]
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        for (key, value) in dict {
            guard let codingKey = CodingKeys(stringValue: key) else {
                continue
            }
            if let enc = value as? EncodableValue {
                try container.encode(enc, forKey: codingKey)
            }
        }
    }

    struct CodingKeys: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
}

let dict: [String: EncodableValue] = ["test": EncodableValue(value:1), "abc":EncodableValue(value:"GOGO")]
let storage = Storage(dict: dict)

do {
    let data = try JSONEncoder().encode(storage)
    let res = String(data: data, encoding: .utf8)
    print(res ?? "nil")
} catch {
    print(error)
}