Introduction to the HandyJSON library

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:

  1. Keep JSON semantics and parse JSON directly, but make the calling method more elegant and safer by encapsulation;
  2. 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:

  1. 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;
  2. 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…

Intelligent Recommendation

handyjson Undefined symbol: _swift_getFieldAt

xcode 11.3 Error running code, Undefined symbol: _swift_getFieldAt Screenshot: Undefined symbols for architecture x86_64: "_swift_getFieldAt", referenced from: HandyJSON.Metadata.Class._prop...

Moya + RxSwift + Handyjson Learning

Moya is a further package of AlamoFire. It is ready to use Moya to implement network requests. Use Moya to simplify network requests, simple and clear, convenient maintenance, and provide convenient u...

The use of iOS SWIFT5 handyjson

Articles directory 1. Dictionary 2. Model 3. Use 1. Dictionary 2. Model 3. Use...

Introduction and introduction of the jQuery library

jQuery library jQuery is a fast and concise JavaScript framework, and is an excellent JavaScript codebase after Prototype (Or JavaScript framework). The goal of jQuery design is "write Less, Do M...

In-depth exploration of HandyJSON (six) KVC

The book is above. The code for this article is still the code that I split the function according to the actual situation. Now that we have been able to assign values ​​to the properties in memory, t...

More Recommendation

A small summary of Swift: Alamofire and HandyJSON

Swift required open source library: Specify the Swift version by entering the following command at the terminal....

Combination of RxSwift, Alamofire, Moya and HandyJson

Project Demo address Create a swift network framework Ready to work Related frameworks needed to use CocoaPods tool Pod The specific pod usage is not explained in detail here, if you don’t under...

Swift Similar Handyjson Analysis Struct

Swift Similar Handyjson Analysis Struct HandyJSON Source Code Analysis Struct Get targetstructMetadata Get targetstructdescriptor Implement TargetRelanchiVedirectPointer FieldDescriptor and FieldRecor...

Dynamic library and static library introduction

(1) Library is related to knowledge points (1) static library 1. Basic concept of static library In essence, the library is a binary form of executable code that can be loaded into memory by the opera...

Introduction to the PIL library in python

[time] 2018.10.19 [Title] Introduction to PIL Library in Python [Reference link] Overview PIL (Python Image Library) is Python's third-party image processing library, but due to its powerful features ...

Copyright  DMCA © 2018-2026 - All Rights Reserved - www.programmersought.com  User Notice

Top