Serialization is a process of converting your instances to another representation, like a string or a stream of bytes. The reverse process of turning the data into an instance is called decoding, or deserialization.
So, you need to encode (or serialize) an instance before you can save it to a file or send it over the web. You need to decode (or deserialize) to bring it back from a file or the web as an instance.
The Encodable
protocol is used by types that can be encoded to another representation. It declares a single method:
func encode(to: Encoder) throws
...which the compiler generates for you if all the stored properties of that type conform to Encodable
as well.
The Decodable
protocol is used by types that can be decoded. It declares just a single initializer:
init(from decoder: Decoder) throws
Codable
is a protocol that a type can conform to, to declare that it can be encoded and decoded. It’s basically an alias for the Encodable
and Decodable
protocols.
typealias Codable = Encodable & Decodable
There are many types in Swift that are codable out of the box: Int
, String
, Date
, Array
and many other types from the Standard Library and the Foundation framework. If you want your type to be codable, the simplest way to do it is by conforming to Codable
and making sure all its stored properties are also codable.
For example, let’s say you have a list of movies. All you need to do to be able to encode and decode this type to conform to the Codable
protocol, like so:
struct Movie: Codable { var title: String var year: Int }
You were able to do it because both title
(String
) and year
(Int
) are codable.
All collections types, like Array
and Dictionary
are also codable if they contain codable types.
JSONEncoder and JSONDecoder
You’ll learn how to encode to and decode from JSON, by using Swift’s JSONEncoder
and JSONDecoder
classes.
JSON stands for JavaScript Object Notation, and is one of the most popular ways to serialize data. It’s easily readable by humans and easy for computers to parse and generate.
Once you have a codable type, you can use JSONEncoder
to convert your type to Data
that can be either written to a file or sent over the network. Assume you have this movie instance:
let movie = Movie(name: "The Shawshank Redemption", year: 1994)
You can encode it, like so:
let jsonEncoder = JSONEncoder() let jsonData = try jsonEncoder.encode(movie)
You’ll notice that you need to use try
because encode(_:)
might fail and throw an error.
If you would like to create a readable version of this JSON as a string, you can use the String
initializer:
let jsonString = String(data: jsonData, encoding: .utf8)! print(jsonString)
If you want to decode the JSON data back into an instance, you need to use JSONDecoder
:
if let dataJson = jsonString?.data(using: .utf8) { let movie1 = try JSONDecoder().decode(Movie.self, from: dataJson) print(movie1) }
Renaming properties with CodingKeys
The CodingKeys
enum, which conforms to CodingKey
protocol, lets you rename specific properties in case the serialized format doesn’t match the requirements of the API.
Add the nested enumeration CodingKeys
like this:
struct Movie: Codable { var title: String var year: Int enum CodingKeys: String, CodingKey { case year = "annum" case title } }
There are several things to note here:
CodingKeys
is a nested enumeration in your type.CodingKey
.String
as the raw type, since the keys must be either strings or integers.Manual encoding and decoding
Codable
is actually just a typealias for the Encodable
and Decodable
protocols. You need to implement encode(to: Encoder)
and describe how to encode each property.
Add this extension:
extension Movie: Encodable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(year, forKey: .year) try container.encode(people?.name, forKey: .producer) } }
Add the following code to your playground to make Movie
conform to Decodable
, and thus also Codable
:
extension Movie: Decodable { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) title = try values.decode(String.self, forKey: .title) year = try values.decode(Int.self, forKey: .year) if let producerTmp = try values.decode(String?.self, forKey: .producer) { people = Person(name: producerTmp) } } }
Decoding JSON dates with custom formats
Dates in JSON are defined as a String or time interval and require a conversion strategy. We can set such a strategy on our JSONDecoder, just like we did for converting camel case to snake case.
Take the following JSON example of a movie:
{ "title": "The Shawshank Redemption", "date": "1994-01-21T09:15:00Z" }
The date in this example is defined with the following format: yyyy-MM-dd'T'HH:mm:ss. We need to create a custom DateFormatter
with this format and apply this to our decoder by setting the dateDecodingStrategy
to formatted
:
struct Movie: Decodable { let title: String let date: Date } let decoder = JSONDecoder() let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" dateFormatter.locale = Locale(identifier: "en_US") dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) decoder.dateDecodingStrategy = .formatted(dateFormatter) let movie: Movie = try! decoder.decode(Movie.self, from: jsonData) print(movie.date) // Prints: 1994-01-21 09:15:00 +0000
There are a few other strategies available to set:
deferredToDate
. Uses Apple’s own data format that tracks the number of seconds and milliseconds since January 1st of 2001. This is mainly useful to use directly with Apple’s platforms.millisecondsSince1970
. This format tracks the number of seconds and milliseconds since January 1st of 1970 and is a lot more common to use.secondsSince1970
. Tracks the numbers of seconds since January 1st of 1970.iso8601
. Decodes the Date as an ISO-8601-formatted string (in RFC 3339 format).