Store Encodables in a Swift Dictionary
Asked Answered
E

2

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?

Eratosthenes answered 20/1, 2018 at 16:21 Comment(0)
G
3

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.

Greenback answered 20/1, 2018 at 17:18 Comment(4)
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.Eratosthenes
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.Eratosthenes
@Eratosthenes - 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). Re why your attempt at encode(to:) didn't work, I obviously cannot comment without seeing what you tried.Greenback
The dateEncodingStrategy issue I mentioned; I've reproduced and asked as a separate question here. If you'd like to take a look.Eratosthenes
S
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)
}
Shum answered 21/1, 2018 at 12:8 Comment(3)
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!Eratosthenes
Vyacheslav, what is the purpose of Storage? Why don't you just encode the dict directly?Greenback
@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.Shum

© 2022 - 2025 — McMap. All rights reserved.