Skip to content

Instantly share code, notes, and snippets.

@ole
Created December 10, 2019 09:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ole/5c31ca9e2919c815029784ef3b8fdc0d to your computer and use it in GitHub Desktop.
Save ole/5c31ca9e2919c815029784ef3b8fdc0d to your computer and use it in GitHub Desktop.
A property wrapper for dictionaries with keys that are raw-representable as strings. It modifies the wrapped dictionary's encoding/decoding behavior such that the dictionary is encoded as a dictionary (unkeyed container) rather than as an array (keyed container). For context, see https://oleb.net/blog/2017/12/dictionary-codable-array/
import Foundation
// MARK: - KeyedContainer
/// A property wrapper for dictionaries with keys that are raw-representable as strings.
/// It modifies the wrapped dictionary's encoding/decoding behavior such that the dictionary
/// is encoded as a dictionary (unkeyed container) rather than as an array (keyed container).
///
/// For context, see <https://oleb.net/blog/2017/12/dictionary-codable-array/>.
@propertyWrapper
struct KeyedContainer<Key, Value> where Key: Hashable & RawRepresentable, Key.RawValue == String {
var wrappedValue: [Key: Value]
/// Copied from the standard library (`_DictionaryCodingKey`).
private struct CodingKeys: CodingKey {
let stringValue: String
let intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
}
extension KeyedContainer: Equatable where Key: Equatable, Value: Equatable {}
extension KeyedContainer: Encodable where Key: Encodable, Value: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in wrappedValue {
let codingKey = CodingKeys(stringValue: key.rawValue)!
try container.encode(value, forKey: codingKey)
}
}
}
extension KeyedContainer: Decodable where Key: Decodable, Value: Decodable {
init(from decoder: Decoder) throws {
wrappedValue = [:]
let container = try decoder.container(keyedBy: CodingKeys.self)
for key in container.allKeys {
let value = try container.decode(Value.self, forKey: key)
wrappedValue[Key(rawValue: key.stringValue)!] = value
}
}
}
// MARK: - Test
enum Color: String, Codable {
case red
case green
case blue
}
struct S: Equatable, Codable {
var unannotatedDict: [Color: String] = [
.red: "ff0000",
.green: "00ff00",
.blue: "0000ff"
]
@KeyedContainer var annotatedDict: [Color: String] = [
.red: "ff0000",
.green: "00ff00",
.blue: "0000ff"
]
}
let input = S()
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let jsonData = try encoder.encode(input)
let jsonText = String(decoding: jsonData, as: UTF8.self)
print(jsonText)
let decoder = JSONDecoder()
let decoded = try decoder.decode(S.self, from: jsonData)
assert(decoded == input)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment