Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Type-Safe World of Codable

The Type-Safe World of Codable

The Type-Safe World of Codable
Codableが導く型安全な世界

try! Swift Tokyo 2018 (#tryswiftconf)
https://www.tryswift.co/events/2018/tokyo/en/

# Source code
https://github.com/tattn/MoreCodable
https://github.com/tattn/DataConvertible

Tatsuya Tanaka

March 02, 2018
Tweet

More Decks by Tatsuya Tanaka

Other Decks in Programming

Transcript

  1. The Type-Safe World of Codable
    Codable͕ಋ͘ܕ҆શͳੈք
    Tatsuya Tanaka / ాதୡ໵ (@tattn)
    #tryswiftconf

    View Slide

  2. • Tatsuya Tanaka / ాத ୡ໵
    • Live in Tokyo, Japan
    • iOS Developer
    • Work at Yahoo! JAPAN
    Tatsuya Tanaka (@tattn)
    @tattn
    @tanakasan2525

    View Slide

  3. Do you use
    Codable?

    View Slide

  4. About Codable
    It's a protocol added in Xcode 9.0.


    We can use Codable after 

    Swift 3.2 / 4.0.
    IUUQTEFWFMPQFSBQQMFDPNEPDVNFOUBUJPOGPVOEBUJPOBSDIJWFT@BOE@TFSJBMJ[BUJPOFODPEJOH@BOE@EFDPEJOH@DVTUPN@UZQFT

    View Slide

  5. Support encoding & decoding any type
    struct User: Codable {
    let id: String
    let name: String
    }
    let user = User(id: "abc", name: "Tatsuya Tanaka")
    let data = try! JSONEncoder().encode(user)
    let json = String(data: data, encoding: .utf8)!
    print(json) // {"id": "abc", "name": "Tatsuya Tanaka"}
    Encode

    View Slide

  6. Support encoding & decoding any type
    struct User: Codable {
    let id: String
    let name: String
    }
    let json = """
    {"id": "abc", "name": "Tatsuya Tanaka"}
    """.data(using: .utf8)!
    let user = try! JSONDecoder().decode(User.self, from: json)
    print(user) // User(id: "abc", name: "Tatsuya Tanaka")
    Decode

    View Slide

  7. Common usage
    Codable is commonly
    used to map JSON to type

    View Slide

  8. Is it used
    only for that?

    View Slide

  9. Codable can be used for 

    a variety of things!
    No

    View Slide

  10. Codable makes your Swift
    more beautiful

    &

    type-safe

    View Slide

  11. Convert any type to Data

    View Slide

  12. Support encoding & decoding any type
    struct User: Codable {
    let id: String
    let name: String
    }
    let user = User(id: "abc", name: "Tatsuya Tanaka")
    let data: Data = try! JSONEncoder().encode(user)
    let json = String(data: data, encoding: .utf8)!
    print(json) // {"id": "abc", "name": "Tatsuya Tanaka"}
    Encode

    View Slide

  13. I don't recommend that 

    you use the code as it is

    View Slide

  14. The effective way to use Codable smartly
    Collaboration with Protocol

    View Slide

  15. Protocol to convert from/to Data
    protocol DataConvertible {
    init(_ data: Data) throws
    func convertToData() throws -> Data
    }

    View Slide

  16. typealias Codable = Decodable & Encodable

    View Slide

  17. Protocol to convert to Data
    extension DataConvertible where Self: Decodable {
    init(_ data: Data) throws {
    self = try JSONDecoder().decode(Self.self, from: data)
    }
    }
    extension DataConvertible where Self: Encodable {
    func convertToData() throws -> Data {
    return try JSONEncoder().encode(self)
    }
    }

    View Slide

  18. Let's use the protocol

    View Slide

  19. Looks good
    struct User: Codable, DataConvertible {
    let id: String
    let name: String
    }
    let user = User(id: "abc", name: "Tatsuya Tanaka")
    let data: Data = try! user.convertToData()
    try! User(data) // User(id: "abc", name: "Tatsuya Tanaka")

    View Slide

  20. More Tips: Add interfaces to UserDefaults
    protocol DataConvertibleStore {
    func set(_ value: DataConvertible, forKey key: String) throws
    func value(_ type: T.Type, forKey key: String) -> T?
    }
    extension UserDefaults: DataConvertibleStore {
    func set(_ value: DataConvertible, forKey key: String) throws {
    let data = try value.convertToData()
    set(data, forKey: key)
    }
    func value(_ type: T.Type = T.self,
    forKey key: String) -> T? {
    let data = self.data(forKey: key)
    return (try? data.map(T.init)).flatMap({ $0 })
    }
    }

    View Slide

  21. So cooool
    let user = User(id: "abc", name: "Tatsuya Tanaka")
    let userDefaults = UserDefaults.standard
    try! userDefaults.set(user, forKey: "user")
    let savedUser = userDefaults.value(User.self,
    forKey: "user")

    View Slide

  22. It brings you an easy and 

    safe way of type conversion.
    Codable has good chemistry with protocol.

    View Slide

  23. Default Encoder / Decoder
    • JSONEncoder / JSONDecoder
    • PropertyListEncoder / PropertyListDecoder

    View Slide

  24. You can create a custom
    encoder / decoder

    View Slide

  25. As an example, 

    I'll introduce my custom
    encoder / decoder

    View Slide

  26. DictionaryEncoder / Decoder
    let user = User(name: "Tatsuya Tanaka", age: 24)
    var encoder = DictionaryEncoder()
    let dictionary: [String: Any] = try! encoder.encode(user)
    print(dictionary["name"] as! String) // Tatsuya Tanaka
    print(dictionary["age"] as! Int) // 24
    var decoder = DictionaryDecoder()
    let user = try! decoder.decode(User.self, from: dictionary)
    print(user) // User(name: "Tatsuya Tanaka", age: 24)

    View Slide

  27. URLQueryItemsEncoder
    struct Parameter: Codable {
    let query: String
    let limit: Int
    }
    let parameter = Parameter(query: "Ͷ͜", limit: 20)
    var encoder = URLQueryItemsEncoder()
    let items: [URLQueryItem] = try! encoder.encode(parameter)
    print(params[0].name) // query
    print(params[0].value) // Ͷ͜
    var urlComponents = URLComponents(string: "https://***.com")!
    urlComponents.queryItems = items
    print(urlComponents.url!)
    // https://***.com?query=%E3%81%AD%E3%81%93&limit=20

    View Slide

  28. Do you want to see the
    implementation?

    View Slide

  29. Implementation of DictionaryEncoder
    import Foundation
    open class DictionaryEncoder: Encoder {
    open var codingPath: [CodingKey] = []
    open var userInfo: [CodingUserInfoKey: Any] = [:]
    private var storage = Storage()
    public init() {}
    open func container(keyedBy type: Key.Type) -> KeyedEncodingContainer {
    return KeyedEncodingContainer(KeyedContainer(encoder: self, codingPath: codingPath))
    }
    open func unkeyedContainer() -> UnkeyedEncodingContainer {
    return UnkeyedContanier(encoder: self, codingPath: codingPath)
    }
    open func singleValueContainer() -> SingleValueEncodingContainer {
    return UnkeyedContanier(encoder: self, codingPath: codingPath)
    }
    private func box(_ value: T) throws -> Any {
    try value.encode(to: self)
    return storage.popContainer()
    }
    }
    extension DictionaryEncoder {
    open func encode(_ value: T) throws -> [String: Any] {
    do {
    return try castOrThrow([String: Any].self, try box(value))
    } catch (let error) {
    throw EncodingError.invalidValue(value,
    EncodingError.Context(codingPath: [],
    debugDescription: "Top-evel \(T.self) did not encode any values.",
    underlyingError: error)
    )
    }
    }
    }
    extension DictionaryEncoder {
    private class KeyedContainer: KeyedEncodingContainerProtocol {
    private var encoder: DictionaryEncoder
    private(set) var codingPath: [CodingKey]
    private var storage: Storage
    init(encoder: DictionaryEncoder, codingPath: [CodingKey]) {
    self.encoder = encoder
    self.codingPath = codingPath
    self.storage = encoder.storage
    storage.push(container: [:] as [String: Any])
    }
    deinit {
    guard let dictionary = storage.popContainer() as? [String: Any] else {
    assertionFailure(); return
    }
    storage.push(container: dictionary)
    }
    private func set(_ value: Any, forKey key: String) {
    guard var dictionary = storage.popContainer() as? [String: Any] else { assertionFailure(); return }
    dictionary[key] = value
    storage.push(container: dictionary)
    }
    func encodeNil(forKey key: Key) throws {}
    func encode(_ value: Bool, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Int, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Int8, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Int16, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Int32, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Int64, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: UInt, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: UInt8, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: UInt16, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: UInt32, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: UInt64, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Float, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: Double, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: String, forKey key: Key) throws { set(value, forKey: key.stringValue) }
    func encode(_ value: T, forKey key: Key) throws {
    encoder.codingPath.append(key)
    defer { encoder.codingPath.removeLast() }
    set(try encoder.box(value), forKey: key.stringValue)
    }
    func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer {
    codingPath.append(key)
    defer { codingPath.removeLast() }
    return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath))
    }
    func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
    codingPath.append(key)
    defer { codingPath.removeLast() }
    return UnkeyedContanier(encoder: encoder, codingPath: codingPath)
    }
    func superEncoder() -> Encoder {
    return encoder
    }
    func superEncoder(forKey key: Key) -> Encoder {
    return encoder
    }
    }
    private class UnkeyedContanier: UnkeyedEncodingContainer, SingleValueEncodingContainer {
    var encoder: DictionaryEncoder
    private(set) var codingPath: [CodingKey]
    private var storage: Storage
    var count: Int { return (storage.last as? [Any])?.count ?? 0 }
    init(encoder: DictionaryEncoder, codingPath: [CodingKey]) {
    self.encoder = encoder
    self.codingPath = codingPath
    self.storage = encoder.storage
    storage.push(container: [] as [Any])
    }
    deinit {
    guard let array = storage.popContainer() as? [Any] else {
    assertionFailure(); return
    }
    storage.push(container: array)
    }
    private func push(_ value: Any) {
    guard var array = storage.popContainer() as? [Any] else { assertionFailure(); return }
    array.append(value)
    storage.push(container: array)
    }
    func encodeNil() throws {}
    func encode(_ value: Bool) throws {}
    func encode(_ value: Int) throws { push(value) }
    func encode(_ value: Int8) throws { push(value) }
    func encode(_ value: Int16) throws { push(value) }
    func encode(_ value: Int32) throws { push(value) }
    func encode(_ value: Int64) throws { push(value) }
    func encode(_ value: UInt) throws { push(value) }
    func encode(_ value: UInt8) throws { push(value) }
    func encode(_ value: UInt16) throws { push(value) }
    func encode(_ value: UInt32) throws { push(value) }
    func encode(_ value: UInt64) throws { push(value) }
    func encode(_ value: Float) throws { push(value) }
    func encode(_ value: Double) throws { push(value) }
    func encode(_ value: String) throws { push(value) }
    func encode(_ value: T) throws {
    encoder.codingPath.append(AnyCodingKey(index: count))
    defer { encoder.codingPath.removeLast() }
    push(try encoder.box(value))
    }
    func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
    codingPath.append(AnyCodingKey(index: count))
    defer { codingPath.removeLast() }
    return KeyedEncodingContainer(KeyedContainer(encoder: encoder, codingPath: codingPath))
    }
    func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
    codingPath.append(AnyCodingKey(index: count))
    defer { codingPath.removeLast() }
    return UnkeyedContanier(encoder: encoder, codingPath: codingPath)
    }
    func superEncoder() -> Encoder {
    return encoder
    }
    }
    }
    final class Storage {
    private(set) var containers: [Any] = []
    var count: Int {
    return containers.count
    }
    var last: Any? {
    return containers.last
    }
    func push(container: Any) {
    containers.append(container)
    }
    @discardableResult
    func popContainer() -> Any {
    precondition(containers.count > 0, "Empty container stack.")
    return containers.popLast()!
    }
    }
    struct AnyCodingKey : CodingKey {
    public var stringValue: String
    public var intValue: Int?
    public init?(stringValue: String) {
    self.stringValue = stringValue
    self.intValue = nil
    }
    public init?(intValue: Int) {
    self.stringValue = "\(intValue)"
    self.intValue = intValue
    }
    init(index: Int) {
    self.stringValue = "Index \(index)"
    self.intValue = index
    }
    static let `super` = AnyCodingKey(stringValue: "super")!
    }
    public enum MoreCodableError: Error {
    case cast
    case unwrapped
    case tryValue
    }
    func castOrThrow(_ resultType: T.Type, _ object: Any, error: Error = MoreCodableError.cast) throws -> T {
    guard let returnValue = object as? T else {
    throw error
    }
    return returnValue
    }
    extension Optional {
    func unwrapOrThrow(error: Error = MoreCodableError.unwrapped) throws -> Wrapped {
    guard let unwrapped = self else {
    throw error
    }
    return unwrapped
    }
    }
    extension Dictionary {
    func tryValue(forKey key: Key, error: Error = MoreCodableError.tryValue) throws -> Value {
    guard let value = self[key] else { throw error }
    return value
    }
    }
    M
    ake
    sense?

    View Slide

  30. IMPOSSIBLE
    in this time
    Custom encoder/decoder tends to be long code...

    View Slide

  31. Don't worry

    View Slide

  32. https://github.com/tattn/MoreCodable
    I published the code!

    View Slide

  33. Not only can Codable
    make it type-safe, 

    but also it can generalize
    type conversion

    View Slide

  34. Codable is
    Hero
    Save the type-
    safe world!

    View Slide

  35. Thank you!

    View Slide