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

Understanding Codable

Understanding Codable

The Encodable and Decodable protocols that were added in Swift 4 provide an easy way to convert Swift types to and from external representations like JSON. In this talk you’ll learn how to use these protocols, and how the Codable system is designed.

Codable is also being adopted in server-side Swift. Learn how to write type-safe Swift web APIs yourself, without having to worry about HTTP content types, headers, and parsing. Easily extract the query parameters from web requests and handle them appropriately in just a few lines of code.

Finally, we’ll look at new enhancements to Codable, coming in Swift 4.1, which will make things even simpler for app developers.

Presented at iOSCon London 2018.

Ian Partridge

March 22, 2018
Tweet

More Decks by Ian Partridge

Other Decks in Programming

Transcript

  1. Ian Partridge • Kitura team leader • Background in runtime

    systems • Java Virtual Machines • Runtime performance analysis • Full-stack debugging • www.twitter.com/alfa • www.github.com/ianpartridge
  2. Serialization for Swift - Design Goals • Tightly integrated with

    Swift’s type system • Bridge the divide between strongly typed language and weakly typed data formats • Comprehensive • Equal support for structs, classes and enums • Follow “Kay’s maxim” • “Simple things should be simple, complex things should be possible”
  3. struct Profile { var name: String var photo: Data var

    dateOfBirth: Date } let profile = Profile(name: “Ian”, photo: image, dateOfBirth: Date(“28-11-1980”))
  4. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } let profile = Profile(name: “Ian”, photo: image, dateOfBirth: Date(“28-11-1980”))
  5. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } let profile = Profile(name: “Ian”, photo: image, dateOfBirth: Date(“28-11-1980”)) let encoder = JSONEncoder() let json: Data = try! encoder.encode(profile) let decoder = JSONDecoder() let person: Profile = try! decoder.decode(Profile.self, from: json)
  6. /// A type that can convert itself into and out

    of an external representation. public typealias Codable = Decodable & Encodable /// A type that can encode itself to an external representation. public protocol Encodable { /// Encodes this value into the given encoder. public func encode(to encoder: Encoder) throws } /// A type that can decode itself from an external representation. public protocol Decodable { /// Creates a new instance by decoding from the given decoder. public init(from decoder: Decoder) throws }
  7. Automatic Codable Conformances • Standard library • Array, Dictionary, Set

    • Int, UInt, Int32…, Float, Double, Bool, String • Optional • CoreGraphics • CGPoint, CGRect, CGSize, CGVector • Foundation • Data, TimeZone, Measurement, UUID, URL, URLComponents, NSRange, Locale, IndexSet, IndexPath, AffineTransform, Decimal, Date, DateComponents, DateInterval, CharacterSet, Calendar
  8. Coding Keys • A private nested enum inside each conforming

    type • One case for every stored property • Synthesised by the compiler automatically whenever possible • Create your own if you want to override the automatic behaviour • Renaming properties to/from the encoded version • Ignoring certain properties during encoding
  9. /// A type that can be used as a key

    for encoding and decoding. public protocol CodingKey { /// The string to use in a named collection /// (e.g. a string-keyed dictionary). var stringValue: String { get } init?(stringValue: String) /// The value to use in an integer-indexed collection /// (e.g. an int-keyed dictionary). var intValue: Int? { get } init?(intValue: Int) }
  10. extension Profile: Encodable { private enum CodingKeys: String, CodingKey {

    case name = "name"
 case photo = "photo" case dateOfBirth = "dateOfBirth" } }
  11. /// A type that can encode values into a native

    format for external representation. public protocol Encoder { /// Returns an encoding container appropriate for holding /// multiple values keyed by the given key type. func container<Key: CodingKey>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> /// Returns an encoding container appropriate for holding /// multiple unkeyed values. func unkeyedContainer() -> UnkeyedEncodingContainer /// Returns an encoding container appropriate for holding /// a single primitive value. func singleValueContainer() -> SingleValueEncodingContainer }
  12. extension Profile: Encodable { func encode(to encoder: Encoder) throws {

    var container = encoder.container(keyedBy: CodingKeys.self)
 try container.encode(self.name, forKey: .name) try container.encode(self.photo, forKey: .photo) try container.encode(self.dateOfBirth, forKey: .dateOfBirth) } } extension Profile: Decodable { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self)
 self.name = try values.decode(String.self, forKey: .name) self.photo = try values.decode(Data.self, forKey: .photo) self.dateOfBirth = try values.decode(Date.self, forKey: .dateOfBirth) } }
  13. $ http -b https://swapi.co/api/ { "films": "https://swapi.co/api/films/", "people": "https://swapi.co/api/people/", "planets":

    "https://swapi.co/api/planets/", "species": "https://swapi.co/api/species/", "starships": "https://swapi.co/api/starships/", "vehicles": "https://swapi.co/api/vehicles/" }
  14. import Foundation struct API: Decodable { let films: URL let

    people: URL let planets: URL let species: URL let starships: URL let vehicles: URL }
  15. import Foundation import PlaygroundSupport let session = URLSession(configuration: .default) let

    apiURL = URL(string: "https://swapi.co/api/")! let request = URLRequest(url: apiURL) let task = session.dataTask(with: request) { data, response, error in guard error == nil, let data = data else { return } let decoder = JSONDecoder() do { let api = try decoder.decode(API.self, from: data) print(api) } catch let error { print(error) } PlaygroundPage.current.finishExecution() } task.resume() PlaygroundPage.current.needsIndefiniteExecution = true
  16. $ http -b https://swapi.co/api/films/ { "count": 7, "next": null, "previous":

    null, "results": [ { "characters": [ "https://swapi.co/api/people/1/", "https://swapi.co/api/people/2/", "https://swapi.co/api/people/3/", "https://swapi.co/api/people/4/", "https://swapi.co/api/people/5/", "https://swapi.co/api/people/6/", "https://swapi.co/api/people/7/", "https://swapi.co/api/people/8/", "https://swapi.co/api/people/9/", "https://swapi.co/api/people/10/", "https://swapi.co/api/people/12/", "https://swapi.co/api/people/13/", "https://swapi.co/api/people/14/", "https://swapi.co/api/people/15/", "https://swapi.co/api/people/16/", "https://swapi.co/api/people/18/", "https://swapi.co/api/people/19/", "https://swapi.co/api/people/81/" ], "created": "2014-12-10T14:23:31.880000Z", "director": "George Lucas", "edited": "2015-04-11T09:46:52.774897Z", "episode_id": 4, "opening_crawl": "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....", "planets": [ "https://swapi.co/api/planets/2/", "https://swapi.co/api/planets/3/", "https://swapi.co/api/planets/1/" ], "producer": "Gary Kurtz, Rick McCallum", "release_date": "1977-05-25", "species": [ "https://swapi.co/api/species/5/", "https://swapi.co/api/species/3/", "https://swapi.co/api/species/2/", "https://swapi.co/api/species/1/", "https://swapi.co/api/species/4/" ], "starships": [ "https://swapi.co/api/starships/2/", "https://swapi.co/api/starships/3/", "https://swapi.co/api/starships/5/", "https://swapi.co/api/starships/9/", "https://swapi.co/api/starships/10/", "https://swapi.co/api/starships/11/", "https://swapi.co/api/starships/12/", "https://swapi.co/api/starships/13/" ], "title": "A New Hope", "url": "https://swapi.co/api/films/1/", "vehicles": [ "https://swapi.co/api/vehicles/4/", "https://swapi.co/api/vehicles/6/", "https://swapi.co/api/vehicles/7/", "https://swapi.co/api/vehicles/8/" ] }, { "characters": [ "https://swapi.co/api/people/2/", "https://swapi.co/api/people/3/", "https://swapi.co/api/people/6/", "https://swapi.co/api/people/7/", "https://swapi.co/api/people/10/", "https://swapi.co/api/people/11/", "https://swapi.co/api/people/20/", "https://swapi.co/api/people/21/", "https://swapi.co/api/people/22/", "https://swapi.co/api/people/33/", "https://swapi.co/api/people/36/", "https://swapi.co/api/people/40/", "https://swapi.co/api/people/43/", "https://swapi.co/api/people/46/", "https://swapi.co/api/people/51/", "https://swapi.co/api/people/52/", "https://swapi.co/api/people/53/", "https://swapi.co/api/people/58/", "https://swapi.co/api/people/59/", "https://swapi.co/api/people/60/", "https://swapi.co/api/people/61/", "https://swapi.co/api/people/62/", "https://swapi.co/api/people/63/", "https://swapi.co/api/people/64/", "https://swapi.co/api/people/65/", "https://swapi.co/api/people/66/", "https://swapi.co/api/people/67/", "https://swapi.co/api/people/68/",
  17. $ http -b https://swapi.co/api/films/ { "count": 7, "next": null, "previous":

    null, "results": [ { "characters": [ /* urls */ ], "created": "2014-12-10T14:23:31.880000Z", "director": "George Lucas", "edited": "2015-04-11T09:46:52.774897Z", "episode_id": 4, "opening_crawl": “…”, "planets": [ /* urls */ ], "producer": "Gary Kurtz, Rick McCallum", "release_date": "1977-05-25", "species": [/* urls */ ], "starships": [ /* urls */ ], "title": "A New Hope", "url": "https://swapi.co/api/films/1/", "vehicles": [ /* urls */ ] },
  18. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episode_id: Int let url: URL }
  19. let session = URLSession(configuration: .default) let filmsURL = URL(string: "https://swapi.co/api/films")!

    let request = URLRequest(url: filmsURL) let task = session.dataTask(with: request) { data, response, error in guard error == nil, let data = data else { return } let decoder = JSONDecoder() do { let films = try decoder.decode(Films.self, from: data) print(films) } catch let error { print(error) } } task.resume()
  20. Films(count: 7, results: [__lldb_expr_54.Film(title: "A New Hope”, episode_id: 4, url:

    https://swapi.co/api/films/1/), __lldb_expr_54.Film(title: "Attack of the Clones", episode_id: 2, url: https://swapi.co/api/films/5/), __lldb_expr_54.Film(title: "The Phantom Menace", episode_id: 1, url: https://swapi.co/api/films/4/), __lldb_expr_54.Film(title: "Revenge of the Sith", episode_id: 3, url: https://swapi.co/api/films/6/), __lldb_expr_54.Film(title: "Return of the Jedi”, episode_id: 6, url: https://swapi.co/api/films/3/), __lldb_expr_54.Film(title: "The Empire Strikes Back”, episode_id: 5, url: https://swapi.co/api/films/2/), __lldb_expr_54.Film(title: "The Force Awakens”, episode_id: 7, url: https://swapi.co/api/films/7/) ])
  21. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episode_id: Int let url: URL }
  22. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episodeID: Int let url: URL enum CodingKeys: CodingKey, String { case title = "title" case episodeID = "episode_id" case url = "url" } }
  23. Films(count: 7, results: [__lldb_expr_65.Film(title: "A New Hope”, episodeID: 4, url:

    https://swapi.co/api/films/1/), __lldb_expr_65.Film(title: "Attack of the Clones", episodeID: 2, url: https://swapi.co/api/films/5/), __lldb_expr_65.Film(title: "The Phantom Menace", episodeID: 1, url: https://swapi.co/api/films/4/), __lldb_expr_65.Film(title: "Revenge of the Sith”, episodeID: 3, url: https://swapi.co/api/films/6/), __lldb_expr_65.Film(title: "Return of the Jedi”, episodeID: 6, url: https://swapi.co/api/films/3/), __lldb_expr_65.Film(title: "The Empire Strikes Back”, episodeID: 5, url: https://swapi.co/api/films/2/), __lldb_expr_65.Film(title: "The Force Awakens”, episodeID: 7, url: https://swapi.co/api/films/7/) ])
  24. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episodeID: Int let url: URL enum CodingKeys: CodingKey, String { case title = "title" case episodeID = "episode_id" case url = "url" } }
  25. let session = URLSession(configuration: .default) let filmsURL = URL(string: "https://swapi.co/api/films")!

    let request = URLRequest(url: filmsURL) let task = session.dataTask(with: request) { data, response, error in guard error == nil, let data = data else { return } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase // New in Swift 4.1! do { let films = try decoder.decode(Films.self, from: data) print(films) } catch let error { print(error) } } task.resume()
  26. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episodeID: Int // Automatically mapped from “episode_id” field let url: URL }
  27. Films(count: 7, results: [__lldb_expr_65.Film(title: "A New Hope”, episodeID: 4, url:

    https://swapi.co/api/films/1/), __lldb_expr_65.Film(title: "Attack of the Clones", episodeID: 2, url: https://swapi.co/api/films/5/), __lldb_expr_65.Film(title: "The Phantom Menace", episodeID: 1, url: https://swapi.co/api/films/4/), __lldb_expr_65.Film(title: "Revenge of the Sith”, episodeID: 3, url: https://swapi.co/api/films/6/), __lldb_expr_65.Film(title: "Return of the Jedi”, episodeID: 6, url: https://swapi.co/api/films/3/), __lldb_expr_65.Film(title: "The Empire Strikes Back”, episodeID: 5, url: https://swapi.co/api/films/2/), __lldb_expr_65.Film(title: "The Force Awakens”, episodeID: 7, url: https://swapi.co/api/films/7/) ])
  28. $ http -b https://swapi.co/api/films/ { "count": 7, "next": null, "previous":

    null, "results": [ { "characters": [ /* urls */ ], "created": "2014-12-10T14:23:31.880000Z", "director": "George Lucas", "edited": "2015-04-11T09:46:52.774897Z", "episode_id": 4, "opening_crawl": “…”, "planets": [ /* urls */ ], "producer": "Gary Kurtz, Rick McCallum", "release_date": "1977-05-25", "species": [/* urls */ ], "starships": [ /* urls */ ], "title": "A New Hope", "url": "https://swapi.co/api/films/1/", "vehicles": [ /* urls */ ] },
  29. struct Films: Decodable { let count: Int let results: [Film]

    } struct Film: Decodable { let title: String let episode_id: Int let release_date: Date let url: URL }
  30. typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [__lldb_expr_9.Films.(CodingKeys in _08D7FDC16B0B899A9D8E5E3183FA3D98).results, Foundation.(_JSONKey in _12768CA107A31EF2DCE034FD75B541C9)(stringValue: "Index 0",

    intValue: Optional(0)), __lldb_expr_9.Film.(CodingKeys in _08D7FDC16B0B899A9D8E5E3183FA3D98).release_date], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
  31. let session = URLSession(configuration: .default) let filmsURL = URL(string: "https://swapi.co/api/films")!

    let request = URLRequest(url: filmsURL) let task = session.dataTask(with: request) { data, response, error in guard error == nil, let data = data else { return } let decoder = JSONDecoder() let formatter = DateFormatter() formatter.dateFormat = "yyyy-mm-dd" decoder.dateDecodingStrategy = .formatted(formatter) do { let films = try decoder.decode(Films.self, from: data) print(films) } catch let error { print(error) } } task.resume()
  32. Films(count: 7, results: [__lldb_expr_65.Film(title: "A New Hope”, episodeID: 4, release_date:

    1977-01-25 00:05:00 +0000 url: https://swapi.co/api/films/1/), __lldb_expr_65.Film(title: "Attack of the Clones", episodeID: 2, release_date: 2002-01-16 00:05:00 +0000 url: https://swapi.co/api/films/5/), __lldb_expr_65.Film(title: "The Phantom Menace", episodeID: 1, release_date: 1999-01-19 00:05:00 +0000 url: https://swapi.co/api/films/4/), __lldb_expr_65.Film(title: "Revenge of the Sith”, episodeID: 3, release_date: 2005-01-19 00:05:00 +0000 url: https://swapi.co/api/films/6/), __lldb_expr_65.Film(title: "Return of the Jedi”, episodeID: 6, release_date: 1983-01-25 00:05:00 +0000 url: https://swapi.co/api/films/3/), __lldb_expr_65.Film(title: "The Empire Strikes Back”, episodeID: 5, release_date: 1980-01-17 00:05:00 +0000 url: https://swapi.co/api/films/2/), __lldb_expr_65.Film(title: "The Force Awakens”, episodeID: 7, release_date: 2015-01-11 00:12:00 +0000 url: https://swapi.co/api/films/7/) ])
  33. DateEncodingStrategy / DateDecodingStrategy • .deferredToDate • .iso8601 • .millisecondsSince1970 •

    .secondsSince1970 • .formatted(DateFormatter) • .custom((Date, Encoder) throws -> Void)
  34. struct Person: Decodable { let name: String let height: Float

    } decoder.nonConformingFloatDecodingStrategy = .convertFromString( positiveInfinity: "+Infinity", negativeInfinity: "-Infinity", nan: "NaN") let person = try! decoder.decode(Person.self, from: json) print(person)
  35. { "name": "", "photo": "", "dateOfBirth": "" } { "name":

    "", "photo": "", "dateOfBirth": { "year": "month": "day": } } struct Profile { var name: String var photo: Data var dateOfBirth: Date } Swift var profile: [String : Any] Swift KITURA
  36. struct Profile { var name: String var photo: Data var

    dateOfBirth: Date } Swift Swift KITURA
  37. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } Swift Swift KITURA
  38. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } Swift Swift struct Profile: Codable { var name: String var photo: Data var dateOfBirth: Date } KITURA
  39. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } Swift Swift struct Profile: Codable { var name: String var photo: Data var dateOfBirth: Date } let encoder = JSONEncoder() let data = try encoder.encode(profile) KITURA
  40. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } Swift Swift struct Profile: Codable { var name: String var photo: Data var dateOfBirth: Date } let decoder = JSONDecoder() let person = try decoder.decode(Person.self, from: jsonData) KITURA
  41. { "name": "", "photo": "", "dateOfBirth": "" } { "name":

    "", "photo": "", "dateOfBirth": "" } struct Profile: Codable { var name: String var photo: Data var dateOfBirth: Date } Swift Swift struct Profile: Codable { var name: String var photo: Data var dateOfBirth: Date } KITURA
  42. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles)
  43. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(request: RouterRequest, response: RouterResponse, next: () -> Void) -> Void { }
  44. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(request: RouterRequest, response: RouterResponse, next: () -> Void) -> Void { var profile = request.read(as: Profile.Type) next() }
  45. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { }
  46. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  47. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) router.post("/profile", handler: addProfile) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  48. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) router.post("/profile", handler: addProfile) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) } func addProfile(profile: Profile, respondWith: @escaping (Profile?, Error?) -> Void) -> Void { ... respondWith(profile, nil) }
  49. guard let backend = KituraKit(baseURL: "http://localhost:8080") else { print("Error creating

    KituraKit client") return } backend.get("/profile") { (profiles: [Profile]?, error: RequestError?) in ... } KITURAKIT
  50. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  51. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } struct Query: QueryParams { var name: String } router.get("/profile", handler: getProfiles) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  52. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } struct Query: QueryParams { var name: String } router.get("/profile", handler: getProfiles) func getProfiles(query: Query, respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  53. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } struct Query: QueryParams { var name: String } router.get("/profile", handler: getProfiles) func getProfiles(query: Query, respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles.filter{ ($0.name == query.name), nil) }
  54. struct Profile: Codable { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(respondWith: @escaping ([Profile]?, Error?) -> Void) -> Void { ... respondWith(profiles, nil) }
  55. struct Profile: Model { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(completion: @escaping ([Profile]?, Error?) -> Void) -> Void { Profile.getAll(completion) }
  56. struct Profile: Model { var name: String var photo: Data

    var dateOfBirth: Date } router.get("/profile", handler: getProfiles) func getProfiles(completion: @escaping ([Profile]?, Error?) -> Void) -> Void { Profile.getAll(completion) }
  57. struct Profile: Model { var name: String var photo: Data

    var dateOfBirth: Date } struct Query: QueryParams { var name: String } router.get("/profile", handler: getProfiles) func getProfiles(completion: @escaping ([Profile]?, Error?) -> Void) -> Void { Profile.getAll(completion) }
  58. struct Profile: Model { var name: String var photo: Data

    var dateOfBirth: Date } struct Query: QueryParams { var name: String } router.get("/profile", handler: getProfiles) func getProfiles(query: Query, completion: @escaping ([Profile]?, Error?) -> Void) -> Void { Profile.getAll(matching: query, completion) }