Skip to content

Instantly share code, notes, and snippets.

@nevillco
Last active October 20, 2023 23:58
Show Gist options
  • Select an option

  • Save nevillco/6a15b5829cbd104f67affc0dbf7fc2d9 to your computer and use it in GitHub Desktop.

Select an option

Save nevillco/6a15b5829cbd104f67affc0dbf7fc2d9 to your computer and use it in GitHub Desktop.
Codable and Custom Keyed Dictionaries
// Code from: https://www.connorneville.com/blog/swift-codable-and-custom-keyed-dictionaries
// Tested against Xcode 15.0 Playground
import Foundation
// MARK: Starting with Codable
struct SocialGroup: Codable {
let id: String
let userIDs: [String]
}
func printJSON<T: Encodable>(_ value: T) {
let jsonData = try! JSONEncoder().encode(value)
print(String(data: jsonData, encoding: .utf8)!)
}
// MARK: Phantom Types
struct ID<Tag>: Codable, Hashable, ExpressibleByStringLiteral {
public var rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
init(stringLiteral value: String) {
self.rawValue = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}
init(from decoder: Decoder) throws {
self.init(try decoder.singleValueContainer().decode(String.self))
}
}
enum UserIDTag { }
enum GroupIDTag { }
typealias UserID = ID<UserIDTag>
typealias GroupID = ID<GroupIDTag>
struct StronglyTypedSocialGroup: Codable {
let id: GroupID
let userIDs: [UserID]
}
let example1 = SocialGroup(
id: "GroupID1",
userIDs: ["UserID1", "UserID2"]
)
print("Example of using Codable with phantom type wrappers:")
printJSON(example1)
print()
// MARK: Fixing Dictionary Encoding with Property Wrappers
@propertyWrapper struct IDKeyed<Tag, Value: Codable>: Codable, ExpressibleByDictionaryLiteral {
var wrappedValue: [ID<Tag>: Value]
init(dictionaryLiteral elements: (ID<Tag>, Value)...) {
wrappedValue = .init(uniqueKeysWithValues: elements)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let untaggedValue = wrappedValue.reduce(into: [String: Value]()) { untaggedDict, next in
untaggedDict[next.key.rawValue] = next.value
}
try container.encode(untaggedValue)
}
init(from decoder: Decoder) throws {
let untaggedValue = try decoder.singleValueContainer().decode([String: Value].self)
self.wrappedValue = untaggedValue.reduce(into: [:]) { taggedDict, next in
taggedDict[ID(next.key)] = next.value
}
}
}
struct WeaklyTypedExample: Codable {
let dictionary: [String: Int]
}
let example2 = WeaklyTypedExample(dictionary: [
"UserID1": 5,
"UserID2": 10
])
print("Example of the default dictionary JSON encoding:")
printJSON(example2)
struct CustomKeyEncodingExample: Codable {
let dictionary: [UserID: Int]
}
let example3 = CustomKeyEncodingExample(dictionary: [
"UserID1": 5,
"UserID2": 10
])
print("Example of the different encoding when keys are not Int or String:")
printJSON(example3)
struct PropertyWrapperFixExample: Codable {
@IDKeyed var dictionary: [UserID: Int]
}
let example4 = PropertyWrapperFixExample(dictionary: [
"UserID1": 5,
"UserID2": 10
])
print("Example of matching the default encoding with a Property Wrapper:")
printJSON(example4)
print()
// MARK: CodingKeyRepresentable
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
init?(stringValue: String) {
self.intValue = nil
self.stringValue = stringValue
}
init<T: CodingKey>(_ key: T) {
self.stringValue = key.stringValue
self.intValue = key.intValue
}
init(_ int: Int) {
self.init(intValue: int)!
}
init(_ string: String) {
self.init(stringValue: string)!
}
}
struct IDWithCodingKey<Tag>: Codable, Hashable, CodingKeyRepresentable, ExpressibleByStringLiteral {
public var rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
init(stringLiteral value: String) {
self.rawValue = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}
init(from decoder: Decoder) throws {
self.init(try decoder.singleValueContainer().decode(String.self))
}
var codingKey: CodingKey {
AnyCodingKey(rawValue)
}
init?<T: CodingKey>(codingKey: T) {
self.rawValue = codingKey.stringValue
}
}
typealias UserIDWithCodingKey = IDWithCodingKey<UserIDTag>
struct CodingKeyFixExample: Codable {
// No more property wrapper!
let dictionary: [UserIDWithCodingKey: Int]
}
let example5 = CodingKeyFixExample(dictionary: [
"UserID1": 5,
"UserID2": 10
])
print("Example of matching the default encoding with CodingKeyRepresentable:")
printJSON(example5)
// Example of using Codable with phantom type wrappers:
// {"id":"GroupID1","userIDs":["UserID1","UserID2"]}
//
// Example of the default dictionary JSON encoding:
// {"dictionary":{"UserID1":5,"UserID2":10}}
// Example of the different encoding when keys are not Int or String:
// {"dictionary":["UserID1",5,"UserID2",10]}
// Example of matching the default encoding with a Property Wrapper:
// {"dictionary":{"UserID2":10,"UserID1":5}}
//
// Example of matching the default encoding with CodingKeyRepresentable:
// {"dictionary":{"UserID1":5,"UserID2":10}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment