Swift Json parsing exploration
In client development projects, it is inevitable to parse network data – to parse JSON data sent from the server into a client-side readable Model. The most used in Objective-C is the JSONModel, which does a good job of parsing based on the OC Runtime. So how does this function work in pure Swift code? Let’s begin our exploration
- Manual parsing
- Native: Swift4.0 JSONDecoder
JSONDecoder
Problems and solutions
Manual parsing
Suppose a User class is parsed, Json looks like this:
{
"userId": 1."name": "Jack"."height": 1.7,}Copy the code
Create a User structure (or class) :
struct User {
var userId: Int?
var name: String?
var height: CGFloat?
}
Copy the code
Convert JSON to User
Before Swift4.0, JSON was modeled by manual parsing. Add a JSON initialization method to User 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
}
}
Copy the code
Extract the specific type of data required by the model from JSON in turn and fill it into the specific properties. If one of the conversions fails or has no value, initialization will fail and return nil.
If a value does not require strong verification, reassign the value directly and remove the statements in the Guard let. For example, if height is not 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}}Copy the code
Native: Swift4.0 JSONDecoder
Swift4.0 was released around June 2017, and one of the major updates was JSON encryption and decryption. Instead of parsing fields manually, you can convert JSON to a Model in a few lines of code. Very similar to JSONModel in Objective-C. Swift4.0 = Swift4.0
struct User: Decodable {
var userId: Int?
var name: String?
var height: CGFloat?
}
let decoder = JSONDecoder(a)if let data = jsonString.data(using: String.Encoding.utf8) {
let user = try? decoder.decode(User.self, from: data)
}
Copy the code
So easy~ and manual parsing differences lie in:
-
Removed handwritten init? Methods. I don’t have to do it manually
-
User implements the Decodable protocol, which is defined 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 } Copy the code
The Decodable protocol has only one method public init(from decoder: decoder) throws decoder instance initialization. Initialization failure 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, eliminating the trouble of manual parsing ~
-
JSONDecoder was used. It is the true parsing tool that dominates the parsing process
Read here, do you feel life from darkness to light ~~
Well, it’s not perfect…
JSONDecoder problems and solutions
Parsing JSON often leads to two inconsistencies:
- The key delivered by the server is inconsistent with that delivered by the server. Procedure For example, if the server delivers key=”order_id”, the end defines key=”orderId”.
- The date expression delivered by the server is
yyyy-MM-dd HH:mm
Or a time stamp, but it’s on the endDate
type - The basic type delivered by the server is inconsistent with that defined on the server. Yes is delivered by the server
String
, defined on the endInt
, etc.
JSONDecoder can solve the first two problems well.
The first key inconsistency problem, JSONDecoder has a ready-made solution. In the example above, assuming that the server returns a key of user_id instead of userId, we can use JSONDecoder’s CodingKeys to convert the property name to the name on encryption and decryption like JSONModel. The User is 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
}
}
Copy the code
Second, the Date conversion problem. JSONDecoder also provides a separate API for us:
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
}
Copy the code
Once the JSONDecoder property dateDecodingStrategy is set, the parse Date type is resolved according to the specified strategy.
Type inconsistency
At this point, JSONDecoder provides us with
- Parsing different
key
The value object Date
The type can be converted by customFloat
In some cases positive and negative infinity and nothing worth special expression. (The probability of occurrence is very small, will not be specified)
JSONDecoder throws a typeMismatch exception that terminates the parse of the data when a basic type is inconsistent with the server (e.g., a number 1, Code Int, server String: “1”).
It’s a little frustrating, but on the end of an application, we want it to be as stable as possible, not something where the whole parsing stops, or even crashes, when a few basic types are inconsistent.
As shown in the table below, we want to handle type mismatches like this: the left column represents the type of the front end, the right column represents the type of the server, and each row represents the types that can be converted from the server when the front end type is X, for example, String can be converted from IntorFloat. These types can basically cover the data delivered by the daily server, while other types of transformations can be expanded according to their own needs.
The front end | The service side |
---|---|
String | Int, Float, |
Float | String |
Double | String |
Bool | String, Int |
JSONDecoder doesn’t have an API to facilitate this kind of exception handling. How to solve it? The most straightforward idea is to implement init(decoder: decoder) manual parsing within a specific model, but it’s too cumbersome to do it all.
Solution:KeyedDecodingContainer
Methods cover
Study JSONDecoder source code, in the process of parsing custom Model, will find such a call relationship.
// The entry method
JSONDecoder decoder(type:Type data:Data)
// The inner class is really used for parsing
_JSONDecoder unbox(value:Any type:Type)
// Model calls the init method
Decodable init(decoder: Decoder)
// The automatically generated init method calls container
Decoder container(keyedBy:CodingKeys)
// Parse the container
KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type)
// Inner class, loop through unbox
_JSONDecoder unbox(value:Any type:Type)... Loop until the base typeCopy the code
The final analysis of _JSONDecoder unbox and KeyedDecodingContainer decoderIfPresent decode method. But _JSONDecoder is an internal class and we can’t handle it. Finally, I decided to do KeyedDecodingContainer, which includes 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)
}
}
Copy the code
DecodeIfPresent (_ type: int. type, forKey key: K) decodeIfPresent(_ type: int. type, forKey key: K) decodeIfPresent(_ type: int. type, forKey key: K) Value. This overwrites the implementation of this function in KeyedDecodingContainer, and now the try? Returns a String value on success, returns a String value on failure, and converts String to Int on success. Okay? Value.
Why write the second function?
Scene: When there are other non-basic models in our Model, such as other custom models, Dictionary
, Array
, etc., exceptions will also be thrown when these Model types do not match or fail, causing the whole large Model parsing to fail.
Override decodeIfPresent
(_ type: t.type, forKey key: K) to avoid these scenarios. So far, when the parsed Optional type does not match during the type process, we either cast it or assign it nil, avoiding the embarrassment of throwing an exception and exiting the whole parse process.
Why not override the decode method? DecodeIfPresent can return Optional values, while decode returns a certain type value. Consider that if the type defined in the Model is no-optional, then it is safe to assume that the developer is sure that the value must exist. If the Model does not exist, it is likely to be an error, so fail.
Complete extension code point me
conclusion
Swift4.0 JSONDecoder does bring great convenience for parsing data. The usage is similar to JSONModel in Objective-C. But the actual development still needs some modifications to better serve us.
If you have any questions or questions about this article, please leave a message at any time