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

Swift 4 Codable (try! Swift NYC 2017)

Swift 4 Codable (try! Swift NYC 2017)

Yasuhiro Inami

September 06, 2017
Tweet

More Decks by Yasuhiro Inami

Other Decks in Programming

Transcript

  1. Swift 4 Codable* Conversion between Swift data structure and archived

    formats * swift-DEVELOPMENT-SNAPSHOT-2017-09-02-a
  2. Codable Example // JSON { "int": 0, "float": 1.5, "double":

    3.25, "string": "Hello", "array": [ "try!", "Swift" ], "dict": { "NYC": 2017 }, "date": 1504711800, }
  3. Step 1. Prepare JSON let json = """ { "int":

    0, "float": 1.5, "double": 3.25, "string": "Hello", "array": [ "try", "Swift" ], "dict": { "NYC": 2017 }, "date": 1504711800, } """.data(using: .utf8)! // ! [SE-0168] Multi-Line String Literals
  4. Step 2. Conform to protocol Codable struct MyData: Codable {

    let int: Int let float: Float let double: Double let string: String let array: [String] let dict: [String: Int] let date: Date }
  5. Step 3. Use decoder & encoder let decoder = JSONDecoder()

    decoder.dateDecodingStrategy = .secondsSince1970 let decoded = try decoder.decode(MyData.self, from: json) // MyData(int: 0, float: 1.5, double: 3.25, string: "Hello", // array: ["try!", "Swift"], dict: ["NYC": 2017], // date: 2017-09-06 15:30:00 +0000) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .secondsSince1970 let encodedData = try encoder.encode(decoded) let encodedString = String(data: encodedData, encoding: .utf8)! // {"double":3.25,"int":0,"string":"Hello","date":1504711800, // "array":["try!","Swift"],"float":1.5,"dict":{"NYC":2017}}
  6. Built-in Codable types • Swift Basic Types • Bool, Int,

    Int(N), UInt(N), Float, Double, String, RawRepresentable, Optional, Array, Set, Dictionary • Foundation / CoreGraphics Types • AffineTransform, Calendar, CharacterSet, Data, Date, DateComponents, DateInterval, Decimal, IndexPath, IndexSet, Locale, Measurement, NSRange, TimeZone, URL, UUID, CGFloat, CGPoint, CGSize, CGRect, ...
  7. Built-in Decoder/Encoder • JSON(De|En)coder ... uses JSONSerialization • date(De|En)codingStrategy •

    data(De|En)codingStrategy • nonConformingFloat(De|En)codingStrategy • PropertyList(De|En)coder ... uses PropertyListSerialization
  8. struct User: Codable { // Auto-synthesis example var userName: String

    var score: Int @derived private enum CodingKeys: String, CodingKey { // @derived = auto-synthesized case userName case score } @derived init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) userName = try container.decode(String.self, forKey: .userName) score = try container.decode(Int.self, forKey: .score) } @derived func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(userName, forKey: .userName) try container.encode(score, forKey: .score) } }
  9. struct User: Codable { // Manual implementation example var userName:

    String // Let's say, JSON has "user_name" key var score: Int // Let's limit to 0...100 private enum CodingKeys: String, CodingKey { // manual implementation case userName = "user_name" // rename key case score } init(from decoder: Decoder) throws { // manual implementation let container = try decoder.container(keyedBy: CodingKeys.self) score = try container.decode(Int.self, forKey: .score) guard (0...100).contains(score) else { // add validation throw DecodingError.dataCorrupted( codingPath: container.codingPath + [CodingKeys.score], debugDescription: "score is not in range 0...100" ) } userName = try container.decode(String.self, forKey: .userName) } }
  10. public typealias Codable = Encodable & Decodable public protocol Decodable

    { init(from decoder: Decoder) throws } public protocol Encodable { func encode(to encoder: Encoder) throws } public protocol CodingKey { public var stringValue: String { get } public init?(stringValue: String) public var intValue: Int? { get } public init?(intValue: Int) }
  11. public protocol Decoder { var codingPath: [CodingKey] { get }

    var userInfo: [CodingUserInfoKey : Any] { get } func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> func unkeyedContainer() throws -> UnkeyedDecodingContainer func singleValueContainer() throws -> SingleValueDecodingContainer } public protocol Encoder { var codingPath: [CodingKey] { get } var userInfo: [CodingUserInfoKey : Any] { get } func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> func unkeyedContainer() -> UnkeyedEncodingContainer func singleValueContainer() -> SingleValueEncodingContainer }
  12. public protocol KeyedDecodingContainerProtocol { associatedtype Key : CodingKey var codingPath:

    [CodingKey] { get } var allKeys: [Key] { get } func contains(_ key: Key) -> Bool func decodeNil(forKey key: Key) throws -> Bool func decode<T : Decodable>(_ type: T.Type, forKey key: Key) throws -> T func decodeIfPresent<T : Decodable> (_ type: T.Type, forKey key: Key) throws -> T? func nestedContainer<NestedKey> (keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer func superDecoder() throws -> Decoder func superDecoder(forKey key: Key) throws -> Decoder ... }
  13. public protocol KeyedEncodingContainerProtocol { associatedtype Key : CodingKey var codingPath:

    [CodingKey] { get } mutating func encodeNil(forKey key: Key) throws mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws mutating func encodeConditional<T : AnyObject & Encodable> (_ object: T, forKey key: Key) throws mutating func encodeIfPresent<T : Encodable> (_ value: T?, forKey key: Key) throws mutating func nestedContainer<NestedKey> (keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer mutating func superEncoder() -> Encoder mutating func superEncoder(forKey key: Key) -> Encoder ... }
  14. public protocol UnkeyedDecodingContainer { var codingPath: [CodingKey] { get }

    var count: Int? { get } var isAtEnd: Bool { get } var currentIndex: Int { get } mutating func decodeNil() throws -> Bool mutating func decode<T : Decodable>(_ type: T.Type) throws -> T mutating func decodeIfPresent<T : Decodable> (_ type: T.Type) throws -> T? mutating func nestedContainer<NestedKey> (keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer mutating func superDecoder() throws -> Decoder ... }
  15. public protocol UnkeyedEncodingContainer { var codingPath: [CodingKey] { get }

    var count: Int { get } mutating func encodeNil() throws mutating func encode<T : Encodable>(_ value: T) throws mutating func encodeConditional<T : AnyObject & Encodable> (_ object: T) throws mutating func encode<T : Sequence>(contentsOf sequence: T) throws where T.Iterator.Element : Encodable mutating func nestedContainer<NestedKey> (keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer mutating func superEncoder() -> Encoder ... }
  16. public protocol SingleValueDecodingContainer { var codingPath: [CodingKey] { get }

    func decodeNil() -> Bool func decode<T : Decodable>(_ type: T.Type) throws -> T ... } public protocol SingleValueEncodingContainer { var codingPath: [CodingKey] { get } mutating func encodeNil() throws mutating func encode<T : Encodable>(_ value: T) throws ... }
  17. public enum DecodingError : Error { public struct Context {

    public let codingPath: [CodingKey] public let debugDescription: String public let underlyingError: Error? } case typeMismatch(Any.Type, Context) case valueNotFound(Any.Type, Context) case keyNotFound(CodingKey, Context) case dataCorrupted(Context) } public enum EncodingError : Error { public struct Context { public let codingPath: [CodingKey] public let debugDescription: String public let underlyingError: Error? } case invalidValue(Any, Context) }
  18. Overview of Codable (1) • Container Protocols • Keyed ...

    for dictionary coding • Unkeyed ... for array coding • SingleValue ... for single primitive value coding • 3 containers are used as intermediate hierarchical representation for arbitrary (de)serialization (e.g. JSONSerialization with [String: Any]).
  19. Overview of Codable (2) • CodingKey • Type-safe String-based key

    (and optionally, Int key) • superDecoder / superEncoder • Used when subclassing • Error Handling (DecodingError / EncodingError) • codingPath records the error path and outputs its detail
  20. // include/swift/AST/KnownProtocols.(h|def) enum class KnownProtocolKind : uint8_t { ... CodingKey,

    Encodable, Decodable, }; // include/swift/AST/(ASTContext.h|KnownIdentifiers.def) class ASTContext { ... Identifier Id_CodingKeys, Identifier Id_Decodable, Identifier Id_decode, Identifier Id_decodeIfPresent, Identifier Id_Decoder, Identifier Id_decoder, Identifier Id_superDecoder, }
  21. // lib/Sema/TypeCheckProtocol.cpp ValueDecl *TypeChecker::deriveProtocolRequirement(DeclContext *DC, NominalTypeDecl *TypeDecl, ValueDecl *Requirement) {

    auto *protocol = cast<ProtocolDecl>(Requirement->getDeclContext()); auto knownKind = protocol->getKnownProtocolKind(); // Derive CodingKey, Encodable, Decodable switch (*knownKind) { ... case KnownProtocolKind::CodingKey: return DerivedConformance::deriveCodingKey(*this, Decl, TypeDecl, Requirement); case KnownProtocolKind::Encodable: return DerivedConformance::deriveEncodable(*this, Decl, TypeDecl, Requirement); case KnownProtocolKind::Decodable: return DerivedConformance::deriveDecodable(*this, Decl, TypeDecl, Requirement); } }
  22. // lib/Sema/DerivedConformanceCodable.cpp // Generates `private enum CodingKeys: CodingKey` static EnumDecl

    *synthesizeCodingKeysEnum(TypeChecker &tc, NominalTypeDecl *target, ProtocolDecl *proto) { auto &C = tc.Context; auto *codingKeyProto = C.getProtocol(KnownProtocolKind::CodingKey); MutableArrayRef<TypeLoc> inherited = C.AllocateCopy(protoTypeLoc); auto *enumDecl = new (C) EnumDecl(SourceLoc(), C.Id_CodingKeys, SourceLoc(), inherited, nullptr, target); enumDecl->setAccessibility(Accessibility::Private); for (auto *varDecl : target->getStoredProperties(true)) { auto *elt = new (C) EnumElementDecl(SourceLoc(), varDecl->getName(), TypeLoc(), /*HasArgumentType=*/false, SourceLoc(), nullptr, enumDecl); enumDecl->addMember(elt); } return enumDecl }
  23. // lib/Sema/DerivedConformanceCodable.cpp // lib/Sema/DerivedConformanceCodingKey.cpp // Generates `init(from decoder: Decoder) throws`

    if not exist static void deriveBodyDecodable_init(...) // Generates `func encode(to encoder: Encoder) throws` if not exist static void deriveBodyEncodable_encode(...) // Generates `private enum CodingKeys: CodingKey` if not exist static EnumDecl *synthesizeCodingKeysEnum(...) // Generates `CodingKey` requirements e.g. `var stringValue: String { ... }` // if `enum CodingKey: CodingKey` exists ValueDecl *DerivedConformance::deriveCodingKey(...)
  24. 1. Requires type variable as argument let myData: MyData let

    decoder = JSONDecoder() myData = try decoder.decode(MyData.self, from: json) Isn't it simple to just infer return type? myData = try decoder.decode(from: json)
  25. 2. Can container be enum? enum JSON { // from

    TryParsec (try! Swift Tokyo 2016) case string(Swift.String) case number(Double) case bool(Swift.Bool) case null case array([JSON]) case object([Swift.String : JSON]) } ... func toJSON() -> JSON { return .object([ "message": .string(message), "user": .object(["name": .string(user.name), "age": .number(user.age)]) ]) }
  26. // Thought experiment using enum intermediate representation indirect enum Container

    { case keyed<K: CodingKey>([K: Container]) // ⚠ generic case! case unkeyed([Container]) case int(Int) // single value case string(String) // single value ... func decode<D: Decodable>(by decoder: Decoder) throws -> D { ... } func encode<E: Encodable>(by encoder: Encoder) throws -> E { ... } } ... func encode(to encoder: Encoder) throws { let container = encoder.keyed([ .message : .string(message), .user : .keyed([.name : .string(user.name), .age : .int(user.age)]) ]) try container.encode(by: encoder) } // More declarative, but performance may be critical // and requires "higher rank type (e.g. generic case)" support.
  27. Recap • Swift 4 Codable is Apple's official MAGIC ✨"✨

    • (Hopefully) ends 3 years of Swift JSON library wars • Beautiful protocol-oriented design • Replaces NSCoder and reflection(Mirror)-based coding • More improvements are possible by [SE-0143] Conditional conformances • Swift 5 will be game changing! #$
  28. Recap • Swift 4 Codable is Apple's official MAGIC ✨"✨

    • (Hopefully) ends 3 years of Swift JSON library wars • Beautiful protocol-oriented design • Replaces NSCoder and reflection(Mirror)-based coding • More improvements are possible by [SE-0143] Conditional conformances • Swift 5 will be game changing! #$