Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active August 12, 2021 20:13
Show Gist options
  • Save IanKeen/c70657e84bd3b8108455ffb18e6e6852 to your computer and use it in GitHub Desktop.
Save IanKeen/c70657e84bd3b8108455ffb18e6e6852 to your computer and use it in GitHub Desktop.
PropertyWrapper: Allow en/de-coding of String keys to non-String types
let json = """
{
"value": {
"true": "foo",
"false": "bar"
}
}
"""
struct Model: Codable {
@LosslessKey var value: [Bool: String]
}
@propertyWrapper
struct LosslessKey<T: Hashable & LosslessStringConvertible, U> {
var wrappedValue: [T: U]
}
extension LosslessKey: Codable where T: Codable, U: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var result: [T: U] = [:]
for (key, value) in try container.decode([String: U].self) {
guard let key = T.init(key) else {
throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode value for key '\(key)'"))
}
result[key] = value
}
self.wrappedValue = result
}
func encode(to encoder: Encoder) throws {
var dict: [String: U] = [:]
for (key, value) in wrappedValue {
dict[key.description] = value
}
try dict.encode(to: encoder)
}
}
/*
This variant requires conforming the key type to a protocol in case you need something that can't (or shouldn't) be LosslessStringConvertible
**/
protocol StringKeyCodable {
static func decode(from string: String, decoder: Decoder) -> Self?
func encode(encoder: Encoder) -> String
}
extension StringKeyCodable where Self: LosslessStringConvertible {
static func decode(from string: String, decoder: Decoder) -> Self? { .init(string) }
func encode(encoder: Encoder) -> String { description }
}
@propertyWrapper
struct StringKey<T: Hashable & StringKeyCodable, U> {
var wrappedValue: [T: U]
}
extension StringKey: Codable where T: Codable, U: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var result: [T: U] = [:]
for (key, value) in try container.decode([String: U].self) {
guard let key = T.decode(from: key, decoder: decoder) else {
throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Unable to decode value for key '\(key)'"))
}
result[key] = value
}
self.wrappedValue = result
}
func encode(to encoder: Encoder) throws {
var dict: [String: U] = [:]
for (key, value) in wrappedValue {
dict[key.encode(encoder: encoder)] = value
}
try dict.encode(to: encoder)
}
}
/// MARK: - Example
let json = """
{
"value": {
"1628798923.073723": "foo",
"1628798920": "bar"
}
}
"""
extension Date: StringKeyCodable {
static func decode(from string: String, decoder: Decoder) -> Date? {
TimeInterval(string).map(Date.init(timeIntervalSince1970:))
}
func encode(encoder: Encoder) -> String {
"\(timeIntervalSince1970)"
}
}
struct Model: Codable {
@StringKey var value: [Date: String]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment