Search code examples
swiftgenericsserializationencodablejsonencoder

Store Encodables in a Swift Dictionary


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?


Solution

  • 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.