The original link: www.raywenderlich.com/3418439-enc…

The most common work in iOS is to save data and transfer it over the network. But before you do that, you need to get the data throughcodingorserializationConvert to the appropriate format.

Again, you need to convert the data into a proper format before you can use it. This reverse process is calleddecodingordeserialization.In this tutorial, you’ll learn everything you need to know to encode and decode using Swift. These include:

  1. inSnake namedandHump namedFormat to format conversion
  2. The customCoding keys
  3. usekeyed.unkeyednestedThe container
  4. To deal withNested types.The date typeAs well as the subclass

That’s a bit much, and it’s time to get started!

start

Download the required resources from here and continue (copy the extract code LGNB first).

After downloading, Starter is the version used for this tutorial. Final is the final version.

We open this section of code Nested types. Make Toy and Employee follow the Codable protocol:

struct Toy: Codable {
  .
}
struct Employee: Codable {
  .
}
Copy the code

Codable is not a protocol in itself, it’s just aliases for two other protocols: Encodable and Decodable. As you might have guessed, these two protocols represent types that can be codeced.

You don’t need to do anything else, because all storage properties for Toy and Employee are Codable. Most types in the Swift standard library (strings, urls, for example) are Codable.

Add a JSONEncoder and JSONDecoder to handle toys and Employees codec:

let encoder = JSONEncoder(a)let decoder = JSONDecoder(a)Copy the code

That’s all we need to do to manipulate JSON! Let’s move on to our first challenge!

Codec nested type

Employee contains a Toy attribute (which is a nested type). The encoded JSON structure is the same as the Employee structure:

{
  "name" : "John Appleseed"."id" : 7."favoriteToy" : {
    "name" : "Teddy Bear"}}Copy the code
public struct Employee: Codable {
  var name: String
  var id: Int
  var favoriteToy: Toy
}
Copy the code

The JSON data nested name in favoriteToy, and all the JSON field names are the same as the storage property names of Toy and Employee. Therefore, the JSON structure is easy to understand based on the type system of the structure. If the property name is the same as the JSON field name, and the properties are Codable, we can easily convert JSON into a data model, or vice versa. Now try it:

/ / 1
let data = try encoder.encode(employee)
/ / 2
let string = String(data: data, encoding: .utf8)!
Copy the code

Two things are done here:

  1. willemployeeuseencode(_:)Encode to JSON. Isn’t it easy?
  2. From the previous stepdataCreate a String and view its contents once.

The encoding here produces legitimate data, so we can use it to recreate Employee:

let sameEmployee = try decoder.decode(Employee.self, from: data)
Copy the code

All right, time for our next challenge!

inSnake namedandHump namedFormat to format conversion

Now, assume that the JSON key name has been converted from a camel (looksLikeThis) format to a snake (looks_like_this_instead) format. However, store attributes for Toy and Employee can only be humped. Fortunately, Foundation has taken this situation into account.

Open the Snake Case vs Camel Case code in this section and add the following code after the codec is created using the previous location:

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase
Copy the code

Run the code and check snakeString. The encoded Employee produces the following:

{
  "name" : "John Appleseed"."id" : 7."favorite_toy" : {
    "name" : "Teddy Bear"}}Copy the code

Custom Coding keys

Now, suppose the JOSN format changes again and uses field names that are different from those stored in Toy and Employee:

{
  "name" : "John Appleseed"."id" : 7."gift" : {
    "name" : "Teddy Bear"}}Copy the code

You can see that gift replaces favoriteToy. In this case, we need to customize Coding keys. Add a special enumerated type to our type. Open Custom Coding Keys and add the following code to Employee:

enum CodingKeys: String.CodingKey {
  case name, id, favoriteToy = "gift"
}
Copy the code

This particular enumeration follows the CodingKey protocol and uses a raw value of type String. Here we match favoriteToy and gift.

During codec, only cases are made in the enumeration, so even those attributes that do not need to be specified need to be included in the enumeration, like name and ID here.

Run playground, and then look at the string value, and you’ll see that the JSON field name is not dependent on the stored property name, thanks to custom Coding keys.

On to the next challenge!

To deal withflatJSON

The JSON format now looks like this:

{
  "name" : "John Appleseed"."id" : 7."gift" : "Teddy Bear"
}
Copy the code

There’s no more nested structure here, it’s not consistent with our model structure. In this case we need to customize the codec process.

Open Keyed containers in this section. There is an Employee type that follows Encodable. We also used Extension to make it Decodable. This has the advantage of preserving the struct’s member by member constructors. If we make Employee Decodable when we define it, it will lose the constructor. Add the following code to Employee:

/ / 1
enum CodingKeys: CodingKey {
  case name, id, gift
}

func encode(to encoder: Encoder) throws {
  / / 2
  var container = encoder.container(keyedBy: CodingKeys.self)
  / / 3
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  / / 4
  try container.encode(favoriteToy.name, forKey: .gift)
}
Copy the code

In the previous simple example (referring to the one-to-one correspondence between attribute names and key names and the same level of nesting), the encode(to:) method is implemented automatically by the compiler. Now we need to implement it manually.

  1. createCodingKeysA field representing JSON. Since we haven’t done any relational mapping, we don’t have to declare that its primitive type isString.
  2. fromencoderTo deriveKeyedEncodingContainerThe container. It’s like a dictionary, where we can store the values of the properties, and that’s how it’s coded.
  3. codingnameandidProperty to the container.
  4. usegiftStudent: the keytoyIs encoded into the container.

Run playground and look at the string value. You’ll see that it matches the JSON format above. We can choose what field name to encode a property value, which gives us a lot of flexibility.

Similar to the coding process, a simple version of the init(from:) method can be implemented automatically by the compiler. But here we need To implement it manually, replacing fatalError(“To do”) with the following code:

/ / 1
let container = try decoder.container(keyedBy: CodingKeys.self)
/ / 2
name = try container.decode(String.self, forKey: .name)
id = try container.decode(Int.self, forKey: .id)
/ / 3
let gift = try container.decode(String.self, forKey: .gift)
favoriteToy = Toy(name: gift)
Copy the code

Then add the following code to recreate employee from JSON:

let sameEmployee = try decoder.decode(Employee.self, from: data)
Copy the code

Handles multi-level nested JSON

The JSON format now looks like this:

{
  "name" : "John Appleseed"."id" : 7."gift" : {
    "toy" : {
      "name" : "Teddy Bear"}}}Copy the code

The name field is in the toy field, and the toy is in the Gift field. How does that translate into the data model that we defined?

Open the code in this section and add the following code to Employee:

/ / 1
enum CodingKeys: CodingKey {  
  case name, id, gift
}
/ / 2
enum GiftKeys: CodingKey {
  case toy
}
/ / 3
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  try container.encode(id, forKey: .id)
  / / 4
  var giftContainer = container
    .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
  try giftContainer.encode(favoriteToy, forKey: .toy)
}
Copy the code

Here are a few things:

  1. Create top-levelCodingKeys
  2. Create for parsinggiftThe fieldCodingKeys, and then use it to create containers
  3. Use top-level container encodingnameandid
  4. usenestedContainer(keyedBy:forKey:)Method for encodinggiftField container, and willfavoriteToyCoding in

Run and look at the string values, and you’ll see that the JSON format is as expected.

The decoding process is similar. Add the following code:

extension Employee: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    id = try container.decode(Int.self, forKey: .id)
    let giftContainer = try container
      .nestedContainer(keyedBy: GiftKeys.self, forKey: .gift)
    favoriteToy = try giftContainer.decode(Toy.self, forKey: .toy)
  }
}

let sameEmployee = try decoder.decode(Employee.self, from: nestedData)
Copy the code

Ok, so we’ve taken care of containers of nested types. And decoded sameEmployee from it.

Processing date type

Now the date field is added to the JSON as follows:

{
  "id" : 7."name" : "John Appleseed"."birthday" : "29-05-2019"."toy" : {
    "name" : "Teddy Bear"}}Copy the code

There is no standard date format in JSON. Used in JSONEncoder and JSONDecoder Date class timeIntervalSinceReferenceDate method to deal with (Date (timeIntervalSinceReferenceDate: interval)).

Here we need to specify the date transition strategy. On Dates, add the following code before try encoder. Encode (employee) :

/ / 1
extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "dd-MM-yyyy"
    return formatter
  }()
}
/ / 2
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
decoder.dateDecodingStrategy = .formatted(.dateFormatter)
Copy the code

There are two main things done here:

  1. inDateFormatterAdded to the extensionformatterIt is formatted to match the format of the date in JSON and is reusable.
  2. Set up thedateEncodingStrategyanddateDecodingStrategyfor.formatted(.dateFormatter), so that the codec will use it to process the date

Run and examine the contents of the dateString, and you’ll see that it meets expectations.

Processing subclasses

The JSON format now looks like this:

{
  "toy" : {
    "name" : "Teddy Bear"
  },
  "employee" : {
    "name" : "John Appleseed"."id" : 7
  },
  "birthday" : 580794178.33482599
}
Copy the code

The information required for Employee is separated here. We are going to use BasicEmployee to resolve Employee. Turn on Subclasses to make BasicEmployee follow Codable:

class BasicEmployee: Codable {
Copy the code

Not surprisingly, the compiler reported an error because GiftEmployee did not follow Codable. We can fix the error by adding the following code:

/ / 1
enum CodingKeys: CodingKey {
  case employee, birthday, toy
}  
/ / 2
required init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  birthday = try container.decode(Date.self, forKey: .birthday)
  toy = try container.decode(Toy.self, forKey: .toy)
  / / 3
  let baseDecoder = try container.superDecoder(forKey: .employee)
  try super.init(from: baseDecoder)
}  
Copy the code

Three things are done here:

  1. inGiftEmployeeAdd theCodingKeys. andJSONCorresponding to the field name in.
  2. fromdecoderDecodes the attribute value of the subclass.
  3. Creates a class that decodes a parent class attributeDecoder, and then call the methods of the parent class to initialize the parent class properties.

Now we continue to complete the GiftEmployee encoding method:

override func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(birthday, forKey: .birthday)
  try container.encode(toy, forKey: .toy)
  let baseEncoder = container.superEncoder(forKey: .employee)
  try super.encode(to: baseEncoder)
}
Copy the code

Similar to the decoding process, we encode the attributes of the subclass, and then get the encoder used to encode the parent class. The following test results:

let giftEmployee = GiftEmployee(name: "John Appleseed", id: 7, birthday: Date(),  toy: toy)
let giftData = try encoder.encode(giftEmployee)
let giftString = String(data: giftData, encoding: .utf8)!
let sameGiftEmployee = try decoder.decode(GiftEmployee.self, from: giftData)
Copy the code

Run and examine the giftString, and you’ll see that the contents are as expected. After learning this section, you will be able to work with more complex inherited data models.

Handles arrays of mixed types

The JSON format now looks like this:

[{"name" : "John Appleseed"."id" : 7
  },
  {
    "id" : 7."name" : "John Appleseed"."birthday" : 580797832.94787002."toy" : {
      "name" : "Teddy Bear"}}]Copy the code

This is a JSON array, but its internal elements are not formatted consistently. Open the code Polymorphic Types in this section and you can see that enumerations are used to define different types of data.

First, we made AnyEmployee follow the Encodable protocol:

enum AnyEmployee: Encodable { . }
Copy the code

Go ahead and add the following code to AnyEmployee:

  / / 1
enum CodingKeys: CodingKey {
  case name, id, birthday, toy
}  
/ / 2
func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  
  switch self {
    case .defaultEmployee(let name, let id):
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
    case .customEmployee(let name, let id, let birthday, let toy):  
      try container.encode(name, forKey: .name)
      try container.encode(id, forKey: .id)
      try container.encode(birthday, forKey: .birthday)
      try container.encode(toy, forKey: .toy)
    case .noEmployee:
      let context = EncodingError.Context(codingPath: encoder.codingPath, 
                                          debugDescription: "Invalid employee!")
      throw EncodingError.invalidValue(self, context)
  }
}
Copy the code

Here we did two main things:

  1. All possible keys are defined.
  2. Encode data according to different types.

To test this, add the following at the end of the code:

let employees = [AnyEmployee.defaultEmployee("John Appleseed".7), 
                 AnyEmployee.customEmployee("John Appleseed".7.Date(),toy)]
let employeesData = try encoder.encode(employees)
let employeesString = String(data: employeesData, encoding: .utf8)!
Copy the code

The coding process that follows is a bit more complicated. Continue adding the following code:

extension AnyEmployee: Decodable {
  init(from decoder: Decoder) throws {
    / / 1
    let container = try decoder.container(keyedBy: CodingKeys.self) 
    let containerKeys = Set(container.allKeys)
    let defaultKeys = Set<CodingKeys>([.name, .id])
    let customKeys = Set<CodingKeys>([.name, .id, .birthday, .toy])
   
    / / 2
   switch containerKeys {
      case defaultKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        self = .defaultEmployee(name, id)
      case customKeys:
        let name = try container.decode(String.self, forKey: .name)
        let id = try container.decode(Int.self, forKey: .id)
        let birthday = try container.decode(Date.self, forKey: .birthday)
        let toy = try container.decode(Toy.self, forKey: .toy)
        self = .customEmployee(name, id, birthday, toy)
      default:
        self = .noEmployee
    }
  }
}
/ / 3
let sameEmployees = try decoder.decode([AnyEmployee].self, from: employeesData) 
Copy the code

Explain the above code:

  1. To obtainKeydContainerAnd get all of its keys.
  2. Depending on the key, different parsing strategies are implemented
  3. fromemployeesDataThe decoded[AnyEmployee]

If the elements in the array can be represented by the same model, but the field may be empty, directly set the model field to optional. Of course, there are also ideas for parsing different models.

Handle arrays

Now, we have JSON in the following format:

[
  "teddy bear"."TEDDY BEAR"."Teddy Bear"
]
Copy the code

Here is an array, and the case is different. In this case, we don’t need any CodingKey, just use unkeyed container.

Open Unkeyed containers and add the following code to the Label structure:

func encode(to encoder: Encoder) throws {
  var container = encoder.unkeyedContainer()
  try container.encode(toy.name.lowercased())
  try container.encode(toy.name.uppercased())
  try container.encode(toy.name)
}
Copy the code

UnkeyedEncodingContainer is similar to the KeyedEncodingContainer we used before, but it doesn’t require a CodingKey because it writes the encoded data to a JSON array. Here we encode 3 different strings into it.

Continue decoding:

extension Label: Decodable {
  / / 1
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var name = ""
    while !container.isAtEnd {
      name = try container.decode(String.self)
    }
    toy = Toy(name: name)
  }
}
let sameLabel = try decoder.decode(Label.self, from: labelData)
Copy the code

UnkeyedContainer gets the last value in the container to initialize the name.

Handles arrays nested within objects

Now we have JSON in the following format:

{
  "name" : "Teddy Bear"."label" : [
    "teddy bear"."TEDDY BEAR"."Teddy Bear"]}Copy the code

This time, the label corresponds to the label field. We need to use nested unkeyed containers for encoding and decoding.

Open the containers in this section and add the following code to the Toy:

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(name, forKey: .name)
  var labelContainer = container.nestedUnkeyedContainer(forKey: .label)                   
  try labelContainer.encode(name.lowercased())
  try labelContainer.encode(name.uppercased())
  try labelContainer.encode(name)
}
Copy the code

Here we create a nested unkeyed container and fill it with three strings. Run the code and look at the string value to see what you expect.

Continue adding the following code to decode:

extension Toy: Decodable {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    var labelContainer = try container.nestedUnkeyedContainer(forKey: .label)
    var labelName = ""
    while !labelContainer.isAtEnd {
      labelName = try labelContainer.decode(String.self)
    }
    label = labelName
  }
}
let sameToy = try decoder.decode(Toy.self, from: data)
Copy the code

Here, we initialize the label field with the last value of the unkeyed container as before, except that we get the nested container.

Process optional fields

Finally, the attributes in our model can also be optional types, and Container provides the corresponding codec methods:

encodeIfPresent(value, forKey: key)
decodeIfPresent(type, forKey: key)
Copy the code

conclusion

Today we took a step-by-step look at how to handle JSON in Swift. The parts of custom Coding keys and processing subclasses need to be emphasized. Hope to help you.