background
JSON is an application layer data exchange protocol commonly used in mobile development. The most common scenario is that the client initiates a network request to the server, and the server returns the JSON text, and then the client parses the JSON text to the specific Model, and then displays the corresponding data to the page.
But when programming, dealing with JSON is a hassle. In iOS development, in the case of not introducing any wheels, it is usually necessary to convert JSON to Dictionary first, and then remember the Key corresponding to each data, and use this Key to take the corresponding Value in the Dictionary to use. In the process of manual analysis, many low-cost errors are often made, such as Key spelling errors, type errors, and key null judgments.
In order to solve these problems, many open source libraries dealing with JSON came into being. By comparison, it can be found that these open source libraries basically need to have two main functions:
- Keep JSON semantics and parse JSON directly, but make the calling method more elegant and safer by encapsulation;
- Predefine the Model class, deserialize JSON into class instances, and use these instances;
In fact, the two points mentioned above are also two functions that must be available in the JSON parsing framework for mobile development. The third-party libraries with the above two points usually include SwiftyJSON, ObjectMapper, JSONNeverDie, HandyJSON, etc., and HandyJSON, which we are going to talk about today, is an open source library that is used in Swift development to convert between Model and JSON. The library was developed by the Alibaba technical team and has accumulated a lot of actual combat.
Advantages of HandyJSON
Before the advent of HandyJSON, there were two main ways to deserialize JSON into the Model class in Swift:
- Let the Model class inherit from NSObject, then the class_copyPropertyList() method gets the property name as the Key, gets the Value from the JSON, and assigns the value to the class property through the KVC mechanism supported by the Objective-C runtime; for example, JSONNeverDie;
- For projects written in pure Swift, you can implement the Mapping function, using overloaded operators for assignment, such as ObjectMapper;
For the above two methods, there are two obvious defects: the former requires the Model to inherit from NSObject, which is very unattractive, and directly negates the way to define the Model with struct; the latter's Mapping function requires the developer to customize, In which the JSON field name corresponding to each attribute is specified, the code intrusion is large, and it is still prone to spelling errors and maintenance difficulties.
HandyJSON has a unique approach, using Swift reflection + memory assignment to construct a Model instance, avoiding the problems encountered by the above two solutions. However, HandyJSON is not perfect, such as frequent memory leaks and poor compatibility.
HandyJSON use
HandyJSON requires the following conditions in the following local environments:
- iOS 8.0+/OSX 10.9+/watchOS 2.0+/tvOS 9.0+
- Swift 3.0+ / Swift 4.0+
At the same time, the version of HandyJSON is different for different IDE environments and Swift versions. You can refer to the following table.
| Xcode | Swift | HandyJSON |
|---|---|---|
| Xcode 10 | Swift 4.2 | 4.2.0 |
| Below Xcode 9.4.1 | Swift 4 | >= 4.1.1 |
| Xcode 8.3 or higher | Swift 3.x | >= 1.8.0 |
HandyJSON installation
For third-party libraries, there are generally two ways to rely, one is the framwork dependency, and the other is the source code dependency. The configuration scripts for installing dependencies using Cocoapods are as follows:
pod 'HandyJSON', '~> 4.2.0'
Copy code
Then execute the "pod install" command to follow the HandyJSON library. Of course, we can also use Carthage to manage third-party frameworks and dependencies.
github "alibaba/HandyJSON" ~> 4.2.0
Copy code
Of course, we can also download the HandyJSON library and rely on the source code. The following address is:github.com/alibaba/Han…
JSON to Model
Basic type
Suppose we get the JSON text from the server like this:
{
"name": "cat",
"id": "12345",
"num": 180
}
Copy code
At this point, if we want to use HandyJSON to deserialize, we only need to define a Model class as follows.
if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
print(animal.name)
print(animal.id)
print(animal.num)
}
Copy code
Complex type
HandyJSON supports the use of various forms of basic properties in class definitions, including optional (?), implicit unpacking optional (!), array (Array), dictionary (Dictionary), Objective-C primitive type (NSString, NSNumber) ), various types of nesting ([Int]?, [String]?, [Int]!, ...) and so on. For example, there is one of the following complex data structures:
struct Cat: HandyJSON {
var id: Int64!
var name: String!
var friend: [String]?
var weight: Double?
var alive: Bool = true
var color: NSString?
}
Copy code
If the data returned in the background is as follows:
{
"id": 1234567,
"name": "Kitty",
"friend": ["Tom", "Jack", "Lily", "Black"],
"weight": 15.34,
"alive": false,
"color": "white"
}
Copy code
If you want to convert the above JSON data to the Model class defined above, you only need one sentence.
let jsonString = "{"id":1234567,"name":"Kitty","friend":["Tom","Jack","Lily","Black"],"weight":15.34,"alive":false,"color":"white"}"
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
print(cat.xxx)
}
Copy code
Model nesting
If a property in the Model class is another custom Model class, then the Transform class can be converted as long as the Model class implements the HandyJSON protocol.
struct Component: HandyJSON {
var aInt: Int?
var aString: String?
}
struct Composition: HandyJSON {
var aInt: Int?
var comp1: Component?
var comp2: Component?
}
let jsonString = "{"num":12345,"comp1":{"aInt":1,"aString":"aaaaa"},"comp2":{"aInt":2,"aString":"bbbbb"}}"
if let composition = JSONDeserializer<Composition>.deserializeFrom(json: jsonString) {
print(composition)
}
Copy code
Specify a node in JSON
Sometimes the JSON text returned by the server contains a lot of state information, such as statusCode, debugMessage, etc., which is usually independent of the Model. Or, we want to parse the data of a specified node in JSON. For this case, HandyJSON is also supported.
struct Cat: HandyJSON {
var id: Int64!
var name: String!
}
/ / JSON returned by the server, we want to parse only the cat in the data
let jsonString = "{"code":200,"msg":"success","data":{"cat":{"id":12345,"name":"Kitty"}}}"
// specify resolution "data.cat"Node data
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
print(cat.name)
}
Copy code
Model that resolves inheritance relationships
If a Model class inherits from another Model class, you only need the parent Model class to implement the HandyJSON protocol.
class Animal: HandyJSON {
var id: Int?
var color: String?
required init() {}
}
class Cat: Animal {
var name: String?
required init() {}
}
let jsonString = "{"id":12345,"color":"black","name":"cat"}"
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
print(cat)
}
Copy code
Custom parsing
Of course, HandyJSON also supports some aspects of custom extensions, which means that HandyJSON allows you to define the parsing key and parsing method of a certain field of the Model class. Perhaps, in JSON parsing, you often encounter the following scenarios:
- When defining a Model, we don't want to use the key agreed by the server as the property name, we want to set one;
- Some types such as enum and tuple cannot be parsed directly from JSON;
For these cases, we can implement custom JSON parsing according to the mapping() function provided by the HandyJSON protocol. For example, there is a Model class and a JSON string returned by a server as follows:
class Cat: HandyJSON {
var id: Int64!
var name: String!
var parent: (String, String)?
required init() {}
}
let jsonString = "{"cat_id":12345,"name":"Kitty","parent":"Tom/Lily"}"
Copy code
It can be seen that the id attribute of the Cat class does not correspond to the Key in the JSON text; for the parent attribute, it is a tuple, which cannot be parsed from the "Tom/Lily" in JSON. So we can use the Mapping function to customize support. At this point, the Model class is as follows:
class Cat: HandyJSON {
var id: Int64!
var name: String!
var parent: (String, String)?
required init() {}
func mapping(mapper: HelpingMapper) {
/ / Specify the id field to use "cat_id" To resolve
mapper.specify(property: &id, name: "cat_id")
/ / Specify the parent field to use this method to resolve
mapper.specify(property: &parent) { (rawString) -> (String, String) in
let parentNames = rawString.characters.split{$0 == "/"}.map(String.init)
return (parentNames[0], parentNames[1])
}
}
}
Copy code
Model to JSON
For Model to JSON, it is relatively simple, and the Model to JSON is the same in Android development.
basic type
If you only need to serialize, you don't need to make any special changes when defining the Model class. Any instance of a class that directly calls HandyJSON's serialization method to serialize it will get a JSON string. E.g:
class Animal {
var name: String?
var height: Int?
init(name: String, height: Int) {
self.name = name
self.height = height
}
}
let cat = Animal(name: "cat", height: 30)
print(JSONSerializer.serializeToJSON(object: cat)!)
print(JSONSerializer.serializeToJSON(object: cat, prettify: true)!)
Copy code
Of course, we can also use the prettify parameter to specify whether the obtained JSON string is formatted.
Complex model
For complex Models, such as the Model Nested Model, we can also serialize the HandyJSON serialization function.
enum Gender: String {
case Male = "male"
case Female = "Female"
}
struct Subject {
var id: Int64?
var name: String?
init(id: Int64, name: String) {
self.id = id
self.name = name
}
}
class Student {
var name: String?
var gender: Gender?
var subjects: [Subject]?
}
let student = Student()
student.name = "Jack"
student.gender = .Female
student.subjects = [Subject(id: 1, name: "math"), Subject(id: 2, name: "English"), Subject(id: 3, name: "Philosophy")]
print(JSONSerializer.serializeToJSON(object: student)!)
print(JSONSerializer.serializeToJSON(object: student, prettify: true)!)
Copy code
Codable
Introduction to Codable
At the WWDC 2017 conference, the release of Swift 4.0 added an important feature: Codable. Codable is a protocol that acts like NSPropertyListSerialization and NSJSONSerialization and is mainly used to complete the conversion between JSON and Model. E.g:
typealias Codable = Decodable & Encodable
public protocol Decodable {
public init(from decoder: Decoder) throws
}
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
Copy code
More knowledge about Codable can be foundOfficial documentIntroduction. As can be seen from the above example, Codable does not exist alone. It is actually a fusion of Decodable and Encodable.
Encoder and decoder
The basic concepts of Encoder and Decoder are similar to NSCoder. An object accepts an encoder and then calls its own method to do the encoding or decoding. The NSCoder API is very straightforward. NSCoder has a number of methods like encodeObject:forKey and encodeInteger:forKey that the object calls to complete the encoding.
Swift's API is not so straightforward. Encoder does not provide an encoding method but provides a container for the encoding to be done. Because of the design of the container, the two protocols Encoder and Decoder are very practical, and only a small amount of information is needed to get the container. For example, here is a wrapper class CodableHelper.swift
rotocol Encoder {
var codingPath: [CodingKey?] { get }
public var userInfo: [CodingUserInfoKey : Any] { get }
func container<Key>(keyedBy type: Key.Type)
-> KeyedEncodingContainer<Key> where Key : CodingKey
func unkeyedContainer() -> UnkeyedEncodingContainer
func singleValueContainer() -> SingleValueEncodingContainer
}
protocol Decoder {
var codingPath: [CodingKey?] { get }
var userInfo: [CodingUserInfoKey : Any] { get }
func container<Key>(keyedBy type: Key.Type) throws
-> KeyedDecodingContainer<Key> where Key : CodingKey
func unkeyedContainer() throws -> UnkeyedDecodingContainer
func singleValueContainer() throws -> SingleValueDecodingContainer
}
Copy code
Instance
Using Codable to parse JSON mainly uses two functions, JSONEncoder and JSONDecoder, where JSONEncoder is used for encoding and JSONDecoder is used for parsing.
let data = try! JSONEncoder().encode([1: 3])
let dict = try! JSONDecoder().decode([Int: Int].self, from: data)
print(dict)
Copy code
basic type
The basic types of Swift's Enum, Struct and Class support Codable. Here is a concrete example.
enum Level: String, Codable {
case large
case medium
case small
}
struct Location: Codable {
let latitude: Double
let longitude: Double
}
// CustomDebugStringConvertible just for better printing
class City: Codable, CustomDebugStringConvertible {
let name: String
let pop: UInt
let level: Level
let location: Location
var debugDescription: String {
return """
{
"name": \(name),
"pop": \(pop),
"level": \(level.rawValue),
"location": {
"latitude": \(location.latitude),
"longitude": \(location.longitude)
}
}
"""
}
}
let jsonData = """
{
"name": "Shanghai",
"pop": 21000000,
"level": "large",
"location": {
"latitude": 30.40,
"longitude": 120.51
}
}
""".data(using: .utf8)!
do {
let city = try JSONDecoder().decode(City.self, from: jsonData)
print("city:", city)
} catch {
print(error.localizedDescription)
}
Copy code
The above example shows the basic usage of the three basic types. It should be noted that all types of storage attributes need to follow the Codable to infer, and the calculated attributes are not subject to this limitation. If the storage attribute does not follow Codable, you need to implement the method in the protocol at the beginning of this article.
Custom key
Since the key of the Codable is directly matched by the attribute name, we need to customize and implement the protocol method when the keys do not match. For example, the above name field is changed to short_name. At this point we need to do this: define an enum method that follows the CodingKey protocol and has a raw value of String and implements Decodable.
let jsonData = """
{
"short_name": "Shanghai", // The key here no longer matches the model
"pop": 21000000,
"level": "large",
"location": {
"latitude": "30.40",
"longitude": 120.51
}
}
""".data(using: .utf8)!
class City: Codable, CustomDebugStringConvertible {
//...the rest of the code is consistent with the previous example
enum CodingKeys: String, CodingKey {
case name = "short_name"
case pop
case level
case location
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
pop = try container.decode(UInt.self, forKey: .pop)
level = try container.decode(Level.self, forKey: .level)
location = try container.decode(Location.self, forKey: .location)
}
}
Copy code
Generic
If the model definition is better, in fact most of the properties can be reused, we can achieve partial reuse of the model through generics.
struct Resource<Attributes>: Codable where Attributes: Codable {
let name: String
let url: URL
let attributes: Attributes
}
struct ImageAttributes: Codable {
let size: CGSize
let format: String
}
Resource<ImageAttributes>
Copy code
Sometimes the format in JSON is not what we actually need, such as floating point numbers in String format, we want to convert to Double type directly when we transfer the model. Then Codable also supports such operations.
struct StringToDoubleConverter: Codable {
let value: Double?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
value = Double(string)
}
}
Copy code
Secondary package
Although Codable can be used to easily convert JSON, it is still not perfect for our project development. We can use secondary packaging.
import Foundation
public extension Encodable {
/ / Object to json string
public func toJSONString() -> String? {
guard let data = try? JSONEncoder().encode(self) else {
return nil
}
return String(data: data, encoding: .utf8)
}
/ / Object to jsonObject
public func toJSONObject() -> Any? {
guard let data = try? JSONEncoder().encode(self) else {
return nil
}
return try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
}
}
public extension Decodable {
//json string to object & array
public static func decodeJSON(from string: String?, designatedPath: String? = nil) -> Self? {
guard let data = string?.data(using: .utf8),
let jsonData = getInnerObject(inside: data, by: designatedPath) else {
return nil
}
return try? JSONDecoder().decode(Self.self, from: jsonData)
}
//jsonObject conversion object or array
public static func decodeJSON(from jsonObject: Any?, designatedPath: String? = nil) -> Self? {
guard let jsonObject = jsonObject,
JSONSerialization.isValidJSONObject(jsonObject),
let data = try? JSONSerialization.data(withJSONObject: jsonObject, options: []),
let jsonData = getInnerObject(inside: data, by: designatedPath) else {
return nil
}
return try? JSONDecoder().decode(Self.self, from: jsonData)
}
}
public extension Array where Element: Codable {
public static func decodeJSON(from jsonString: String?, designatedPath: String? = nil) -> [Element?]? {
guard let data = jsonString?.data(using: .utf8),
let jsonData = getInnerObject(inside: data, by: designatedPath),
let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [Any] else {
return nil
}
return Array.decodeJSON(from: jsonObject)
}
public static func decodeJSON(from array: [Any]?) -> [Element?]? {
return array?.map({ (item) -> Element? in
return Element.decodeJSON(from: item)
})
}
}
/ / According to the designedPath to get the data in the object
fileprivate func getInnerObject(inside jsonData: Data?, by designatedPath: String?) -> Data? {
guard let _jsonData = jsonData,
let paths = designatedPath?.components(separatedBy: "."),
paths.count > 0 else {
return jsonData
}
/ / Remove the jsonObject specified by designatedPath from jsonObject
let jsonObject = try? JSONSerialization.jsonObject(with: _jsonData, options: .allowFragments)
var result: Any? = jsonObject
var abort = false
var next = jsonObject as? [String: Any]
paths.forEach({ (seg) in
if seg.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) == "" || abort {
return
}
if let _next = next?[seg] {
result = _next
next = _next as? [String: Any]
} else {
abort = true
}
})
/ / Judge the condition to ensure that the correct result is returned, to ensure that there is no abortion, to ensure that jsonObject is converted to Data type
guard abort == false,
let resultJsonObject = result,
let data = try? JSONSerialization.data(withJSONObject: resultJsonObject, options: []) else {
return nil
}
return data
}
Copy code
The use of CodableHelper is also very simple, a little object-oriented feeling, the following is a specific use case.
struct Person: Codable {
var name: String?
var age: Int?
var sex: String?
}
//jsonString gets the data encapsulated into a Model
let p1String = "{"name":"walden","age":30,"sex":"man"}"
let p1 = Person.decodeJSON(from: p1String)
//jsonString gets the data encapsulated into an Array
let personString = "{"haha":[{"name":"walden","age":30,"sex":"man"},{"name":"healer","age":20,"sex":"female"}]}"
let persons = [Person].decodeJSON(from: personString, designatedPath: "haha")
/ / Object to jsonString
let jsonString = p1?.toJSONString()
/ / Object to jsonObject
let jsonObject = p1?.toJSONObject()
Copy code
Attached:swift.ctolib.com/HandyJSON.h…