Detailed analysis of Swift Json instance

  • 2020-06-03 08:33:39
  • OfStack

preface

In client development projects, it is inevitable to parse network data -- JSON data sent by the server into Model readable by the client. JSONModel is the most commonly used one under ES6en-ES7en, which can perform parsing work well under OC Runtime. So how does this work in pure Swift code? Let's begin our exploration

Manual parsing Native: Swift4.0 JSONDecoder JSONDecoder issues and solutions

Manual parsing

Suppose that an User class is resolved, Json is as follows:


{
 "userId": 1,
 "name": "Jack",
 "height": 1.7,
}

Corresponding to creating 1 User structure (or class) :


struct User {
 var userId: Int?
 var name: String?
 var height: CGFloat?
}

Convert JSON to User

Before Swift4.0, we manually parse JSON model. Add an initialization method to User that takes JSON as the parameter. The code is as follows:


struct User {
 ...
 init?(json: [String: Any]) {
  guard let userId = json["userId"] as? Int,
  let name = json["name"] as? String,
  let height = json["height"] as? CGFloat else { return nil }
  self.userId = userId
  self.name = name
  self.height = height
 }
}

In turn, the specific type of data required by model is extracted from json and filled into the corresponding property. If one of the transformations fails or has no value, initialization fails to return nil.

If a value does not require strong validation, simply assign the value and remove the statement in guard let. For example, if height does not need to be checked, look at the following code:


struct User {
 ...
 init?(json: [String: Any]) {
  guard let userId = json["userId"] as? Int,
  let name = json["name"] as? String else { return nil }
  self.userId = userId
  self.name = name
  self.height = json["height"] as? CGFloat
 }
}

Native: Swift4.0 JSONDecoder

Swift4.0 was released around June 2017, among which one major update is the encryption and decryption of JSON. Instead of parsing fields by hand, you can convert JSON to Model in a few lines of code. Very similar to JSONModel under ES67en-ES68en. Similarly, User, Swift4.0 in the above example can be written as follows:


struct User: Decodable {
 var userId: Int?
 var name: String?
 var height: CGFloat?
}

let decoder = JSONDecoder()
if let data = jsonString.data(using: String.Encoding.utf8) {
 let user = try? decoder.decode(User.self, from: data)
}

The difference between so easy~ and manual parsing lies in:

1. Remove handwriting. Methods. I don't have to do it manually

2.User implements Decodable protocol, the definition of which is as follows:


/// A type that can decode itself from an external representation.
public protocol Decodable {
 /// Creates a new instance by decoding from the given decoder.
 ///
 /// This initializer throws an error if reading from the decoder fails, or
 /// if the data read is corrupted or otherwise invalid.
 ///
 /// - Parameter decoder: The decoder to read data from.
 public init(from decoder: Decoder) throws
}

The Decodable protocol has only one method public init(from decoder: Decoder) throws -- Initialize as an instance of Decoder. Failure to initialize may throw an exception. Fortunately, as long as the Decodable protocol is inherited, the system will automatically detect the attributes in the class for initialization, which saves the trouble of manual parsing

3. JSONDecoder was used. It is a true parsing tool that dominates the entire parsing process

Read here, not feel life from the dark to the light ~~

However, it's not perfect...

JSONDecoder issues and programmes

JSON often encounters two problems:

The key sent by the server is not the same as that sent by the server. For example, the server sends key="order_id" and defines key="orderId" The date expression issued by the server is ES116en-ES117en-ES118en HH:mm or timestamp, but the end is of type Date The base type sent by the server is not the same as the one defined on the server. The server issues String, the Int defined on the end, etc

The first two problems, JSONDecoder, are well solved.

The first key does not pose a problem, and JSONDecoder has a ready-made solution. For the example described above, assuming that the server returns key as user_id instead of userId, we can use CodingKeys of JSONDecoder to convert the property name as JSONModel1 does during encryption and decryption. User has been modified as follows:


struct User: Decodable {
 var userId: Int?
 var name: String?
 var height: CGFloat? 
 enum CodingKeys: String, CodingKey {
  case userId = "user_id"
  case name
  case height
 }
}

Second, Date conversion problem. JSONDecoder also provides us with a separate API:


open class JSONDecoder {
 /// The strategy to use for decoding `Date` values.
 public enum DateDecodingStrategy {

  /// Defer to `Date` for decoding. This is the default strategy.
  case deferredToDate

  /// Decode the `Date` as a UNIX timestamp from a JSON number.
  case secondsSince1970

  /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
  case millisecondsSince1970

  /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
  case iso8601

  /// Decode the `Date` as a string parsed by the given formatter.
  case formatted(DateFormatter)

  /// Decode the `Date` as a custom value decoded by the given closure.
  case custom((Decoder) throws -> Date)
 }
 ......
 /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
 open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
}

Once the JSONDecoder attribute dateDecodingStrategy is set, the Date type is parsed according to the specified policy.

Type not 1

At this point, JSONDecoder provides us with

Resolve different key value objects The Date type can be customized for conversion Float is a special representation of plus or minus infinity and no merit in 1. (The probability of occurrence is very small, not to elaborate)

However, in the case of a difference between the base type and the server (for example, a number 1, Code on the end is of TYPE Int, and the server issues String: "1"), JSONDecoder will throw an typeMismatch exception and end the parsing of the whole data.

This is a bit annoying, but we want the app to be as stable as possible, rather than having the entire parse stop, or even Crash, in some cases after encountering several basic types.

As shown in the table below, we want to be able to do this if the types do not match: the left column represents the type of the front end, the right column represents the type of the server, and each row represents what type can be converted from the server when the front end is X, for example, String can be converted from IntorFloat. These types can basically cover the data issued by the daily server, while other types of transformation can be expanded according to their own needs.


前端
服务端
String Int,Float
Float String
Double String
Bool String, Int

JSONDecoder does not give us the convenience of this exception handling of API. How to solve it? The most straightforward idea would be to implement init(decoder: Decoder) manually within a specific model, but each would be too cumbersome to do.

Solution: KeyedDecodingContainer method override

Study the source code of JSONDecoder, in the process of parsing custom Model, you will find such a call relationship.


//  Entry method 
JSONDecoder decoder(type:Type data:Data) 
 //  Inner class, real for parsing 
 _JSONDecoder unbox(value:Any type:Type) 
 // Model call init methods 
 Decodable init(decoder: Decoder) 
 //  Autogenerated init The method call container
 Decoder container(keyedBy:CodingKeys) 
 //  Parsed container 
 KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type)
  //  Inner class, circular call unbox
  _JSONDecoder unbox(value:Any type:Type)
  ... Loop until you get to the base type 

The final parsing falls to the unbox method of _JSONDecoder and decoderIfPresent decode method of KeyedDecodingContainer. But _JSONDecoder is an inner class and we can't handle it. The final decision to go for KeyedDecodingContainer included the following code:


extension KeyedDecodingContainer {
 .......
 /// Decode (Int, String) -> Int if possiable
 public func decodeIfPresent(_ type: Int.Type, forKey key: K) throws -> Int? {
  if let value = try? decode(type, forKey: key) {
   return value
  }
  if let value = try? decode(String.self, forKey: key) {
   return Int(value)
  }
  return nil
 }
 
 .......
 
 /// Avoid the failure just when decoding type of Dictionary, Array, SubModel failed
 public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable {
  return try? decode(type, forKey: key)
 }
}

The first function in the above code decodeIfPresent(_ type: Int.Type, forKey key: K) Is Int derived from key? Value. This overrides the implementation of this function in KeyedDecodingContainer, now try? Is resolved in the form of Int type. If it succeeds, it will be returned directly. If it fails, it will be resolved in the form of String type. Value.

Why write the second function?

Scenario: When we have other non-base types of Model inside Model, such as other custom Model, Dictionary < String, Any > , Array < String > When these Model types do not match or make an error, an exception will be thrown, causing the entire Model parsing to fail.
cover decodeIfPresent<T>(_ type: T.Type, forKey key: K) You can avoid these scenarios. So far, when the resolved Optional type does not match in the type process, we either transform it or assign nil to it, so as to avoid the embarrassment of the system quitting the whole parsing process due to throw exception.

Why not override the decode method? decodeIfPresent can return the Optional value, and decode returns a defined type value. Given that if the type defined within Model is OF type No-ES256en, then the developer can be assumed to be certain that the value must exist, it would probably be an error if Model does not exist, so fail directly.

Full extension code point Me (local download point Me)

conclusion

Swift4.0 JSONDecoder does offer great convenience for parsing data. The usage is similar to JSONModel under Objective-C. However, in the actual development, we still need some modifications to better serve us.


Related articles: