3

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?

justintime
  • 352
  • 3
  • 11

2 Answers2

2

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I used the first approach as it was minimally invasive to my current code. It works! Thanks a lot. Can you shed some light on why at run time Swift is able to find out what type of Encodable our 'value' is? I had earlier tried a similar approach but hadn't declared the `func encode(to encoder: Encoder) throws` in the EncodableValue struct, which didn't work. – justintime Jan 22 '18 at 09:40
  • There is one thing I noticed however, `JSONEncoder` provides a `dateEncodingStrategy`, where you can specify the format of how you want to show your Encodable Date object. That fails here, and it falls back to the default strategy where it's convert to number of timeInterval since some date. – justintime Jan 22 '18 at 10:48
  • @justintime - Re your first question, the approach underlying EncodableValue is that we're using the protocol as the type (see [The Swift Programming Language: Protocols as Types](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID275)). Re why your attempt at `encode(to:)` didn't work, I obviously cannot comment without seeing what you tried. – Rob Jan 22 '18 at 15:44
  • The `dateEncodingStrategy` issue I mentioned; I've reproduced and asked as a separate question [here](https://stackoverflow.com/questions/48658574/jsonencoders-dateencodingstrategy-not-working). If you'd like to take a look. – justintime Feb 07 '18 at 07:58
0

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)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Vyacheslav
  • 26,359
  • 19
  • 112
  • 194
  • Don't fully get it but seems like you're using dynamic keys to make Storage encodable at run time. Interesting, will save this for later. Thanks! – justintime Jan 22 '18 at 11:14
  • Vyacheslav, what is the purpose of `Storage`? Why don't you just `encode` the `dict` directly? – Rob Jan 22 '18 at 14:36
  • @Rob, I think you are right. That was the first implementation. By the way, I will not remove this answer due to there are no good examples for swift 4 JSON dynamic parsing. – Vyacheslav Jan 22 '18 at 20:40