Json开源:Serpent - 基于协议的对象和结构的正、反向序列化 JSON 及转换库
yangjiabi
8年前
<p style="text-align: center;"><img src="https://simg.open-open.com/show/771759396e0d3fa5f11b5fb89c2f3d93.png"></p> <p> </p> <p>Serpent <em>(previously known as Serializable)</em> is a framework made for creating model objects or structs that can be easily serialized and deserialized from/to JSON. It's easily expandable and handles all common data types used when consuming a REST API, as well as recursive parsing of custom objects. Designed for use with Alamofire.</p> <p>It's designed to be used together with our helper app, the Model Boiler , making model creation a breeze.</p> <p>Serpent is implemented using protocol extensions and static typing.</p> <h2>:bookmark_tabs: Table of Contents</h2> <ul> <li>:snake: Why Serpent?</li> <li>:memo: Requirements</li> <li>:package: Installation <ul> <li>Carthage</li> <li>CocoaPods</li> <li>Swift Package Manager</li> </ul> </li> <li>:wrench: Setup</li> <li>:computer: Usage <ul> <li>Getting started</li> <li>Using Serpent models</li> <li>More complex examples</li> <li>Using with Alamofire</li> <li>Date parsing</li> </ul> </li> <li>:busts_in_silhouette: Credits</li> <li>:page_facing_up: License</li> </ul> <h2>:snake: Why Serpent?</h2> <p>There are plenty of other Encoding and Decoding frameworks available. Why should you use Serpent?</p> <ul> <li><a href="/misc/goto?guid=4959742082002395616" rel="nofollow,noindex">Performance</a> . Serpent is fast, up to 4x faster than similar frameworks.</li> <li><a href="/misc/goto?guid=4959742082092139471" rel="nofollow,noindex">Features</a> . Serpent can parse anything you throw at it. Nested objects, Enums, URLs, UIColor, you name it!</li> <li><a href="/misc/goto?guid=4959742082174621684" rel="nofollow,noindex">Model Boiler</a> . Every framework of this kind requires tedious boilerplate code that takes forever to write. <a href="/misc/goto?guid=4959742082174621684" rel="nofollow,noindex">Model Boiler</a> generates it for you instantly.</li> <li><a href="/misc/goto?guid=4959742082279472927" rel="nofollow,noindex">Alamofire Integration</a> . Using the included Alamofire extensions makes implementing an API call returning parsed model data as simple as doing a one-liner!</li> <li><a href="/misc/goto?guid=4959742082279472927" rel="nofollow,noindex">Expandability</a> . Parsing into other datatypes can easily be added.</li> <li><a href="/misc/goto?guid=4959742082279472927" rel="nofollow,noindex">Persisting</a> . Combined with our caching framework <a href="/misc/goto?guid=4959742082378809829" rel="nofollow,noindex">Cashier</a> , Serpent objects can be very easily persisted to disk.</li> <li><img src="https://simg.open-open.com/show/bd0ee0d3195a596ce524c4dc9486446b.png"> makes it easier to create the model files in Xcode.</li> </ul> <h2>:memo: Requirements</h2> <ul> <li>iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+</li> <li>Swift 3.0+<br> <em>(Swift 2.2 & Swift 2.3 supported in older versions)</em></li> </ul> <h2>:package: Installation</h2> <h3>Carthage</h3> <pre> github "nodes-ios/Serpent" ~> 1.0</pre> <p>Last versions compatible with lower Swift versions:</p> <p>Swift 2.3</p> <p>github "nodes-ios/Serpent" == 0.13.2</p> <p>Swift 2.2</p> <p>github "nodes-ios/Serpent" == 0.11.2</p> <p>NOTE:Serpent was previously known as <strong>Serializable</strong> .</p> <h3>CocoaPods</h3> <p>Choose one of the following, add it to your Podfile and run pod install :</p> <pre> pod 'Serpent', '~> 1.0' # Just core pod 'Serpent/Extensions', '~> 1.0' # Includes core and all extensions pod 'Serpent/AlamofireExtension', '~> 1.0' # Includes core and Alamofire extension pod 'Serpent/CashierExtension', '~> 1.0' # Includes core and Cashier extension</pre> <p>NOTE:CocoaPods only supports Serpent using Swift version 3.0 and higher.</p> <h3>Swift Package Manager</h3> <p>To use Serpent as a <a href="/misc/goto?guid=4958973377050938320" rel="nofollow,noindex">Swift Package Manager</a> package just add the following to your Package.swift file.</p> <pre> import PackageDescription let package = Package( name: "YourPackage", dependencies: [ .Package(url: "https://github.com/nodes-ios/Serpent.git", majorVersion: 1) ] )</pre> <h2>:wrench: Setup</h2> <p>We <strong>highly</strong> recommend you use our <a href="/misc/goto?guid=4959742082174621684" rel="nofollow,noindex">Model Boiler</a> to assist with generating the code needed to conform to Serpent. Instructions for installation and usage can be found at the <a href="/misc/goto?guid=4959742082174621684" rel="nofollow,noindex">Model Boiler GitHub repository</a> .</p> <h2>:computer: Usage</h2> <h3>Getting started</h3> <p>Serpent supports all primitive types, enum , URL , Date , UIColor , other Serpent model, and Array of all of the aforementioned types. Your variable declarations can have a default value or be optional.</p> <p>Primitive types do not need to have an explicit type, if Swift is able to infer it normally. var name: String = "" works just as well as var name = "" . Optionals will of course need an explicit type.</p> <p>NOTE:Enums you create must conform to RawRepresentable , meaning they must have an explicit type. Otherwise, the parser won't know what to do with the incoming data it receives.</p> <p>Create your model struct or class:</p> <pre> struct Foo { var id = 0 var name = "" var address: String? }</pre> <p>NOTE:Classes must be marked final .</p> <p>Add the required methods for Encodable and Decodable :</p> <pre> extension Foo: Serializable { init(dictionary: NSDictionary?) { id <== (self, dictionary, "id") name <== (self, dictionary, "name") address <== (self, dictionary, "address") } func encodableRepresentation() -> NSCoding { let dict = NSMutableDictionary() (dict, "id") <== id (dict, "name") <== name (dict, "address") <== address return dict } }</pre> <p>NOTE:You can add conformance to Serializable which is a type alias for both Encodable and Decodable .</p> <p>And thats it! If you're using the Model Boiler , this extension will be generated for you, so that you don't need to type it all out for every model you have.</p> <h3>Using Serpent models</h3> <p>New instances of your model can be created with a dictionary, e.g. from parsed JSON.</p> <pre> let dictionary = try JSONSerialization.jsonObject(with: someData, options: .allowFragments) as? NSDictionary let newModel = Foo(dictionary: dictionary)</pre> <p>You can generate a dictionary version of your model by calling encodableRepresentation() :</p> <pre> let encodedDictionary = newModel.encodableRepresentation()</pre> <h3>More complex examples</h3> <p>In this example, we have two models, Student and School.</p> <pre> struct Student { enum Gender: String { case male = "male" case female = "female" case unspecified = "unspecified" } var name = "" var age: Int = 0 var gender: Gender? } struct School { enum Sport: Int { case football case basketball case tennis case swimming } var name = "" var location = "" var website: URL? var students: [Student] = [] var sports: [Sport]? }</pre> <p>You can get as complicated as you like, and the syntax will always remain the same. The extensions will be:</p> <pre> extension Student: Serializable { init(dictionary: NSDictionary?) { name <== (self, dictionary, "name") age <== (self, dictionary, "age") gender <== (self, dictionary, "gender") } func encodableRepresentation() -> NSCoding { let dict = NSMutableDictionary() (dict, "name") <== name (dict, "age") <== age (dict, "gender") <== gender return dict } } extension School: Serializable { init(dictionary: NSDictionary?) { name <== (self, dictionary, "name") location <== (self, dictionary, "location") website <== (self, dictionary, "website") students <== (self, dictionary, "students") sports <== (self, dictionary, "sports") } func encodableRepresentation() -> NSCoding { let dict = NSMutableDictionary() (dict, "name") <== name (dict, "location") <== location (dict, "website") <== website (dict, "students") <== students (dict, "sports") <== sports return dict } }</pre> <p>Again, the <a href="/misc/goto?guid=4959742082174621684" rel="nofollow,noindex">Model Boiler</a> generates all of this code for you in less than a second!</p> <h3>Using with Alamofire</h3> <p>Serpent comes integrated with Alamofire out of the box, through an extension on Alamofire's Request construct, that adds the function responseSerializable(completion:unwrapper)</p> <p>The extension uses Alamofire's familiar Response type to hold the returned data, and uses its generic associated type to automatically parse the data.</p> <p>Consider an endpoint returning a single school structure matching the struct from the example above. To implement the call, simply add a function to your shared connection manager or where ever you like to put it:</p> <pre> func requestSchool(completion: @escaping (DataResponse<School>) -> Void) { request("http://somewhere.com/school/1", method: .get).responseSerializable(completion) }</pre> <p>In the consuming method you use it like this:</p> <pre> requestSchool() { (response) in switch response.result { case .success(let school): //Use your new school object! case .failure(let error): //Handle the error object, or check your Response for more detail } }</pre> <p>For an array of objects, use the same technique:</p> <pre> static func requestStudents(completion: @escaping (DataResponse<[Student]>) -> Void) { request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion) }</pre> <p>Some APIs wrap their data in containers. Use the unwrapper closure for that. Let's say your /students endpoint returns the data wrapped in a students object:</p> <pre> { "students" : [ { "..." : "..." }, { "..." : "..." } ] }</pre> <p>The unwrapper closure has 2 input arguments: The sourceDictionary (the JSON Response Dictionary) and the expectedType (the <em>type</em> of the target Serpent). Return the object that will serve as the input for the Serializable initializer.</p> <pre> static func requestStudents(completion: (DataResponse<[Student]>) -> Void) { request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion, unwrapper: { $0.0["students"] }) }</pre> <p>If you need to unwrap the response data in every call, you can install a default unwrapper using</p> <pre> Parser.defaultWrapper = { sourceDictionary, expectedType in // You custom unwrapper here... return sourceDictionary }</pre> <p>The expectedType can be used to dynamically determine the key based on the type name using reflection. This is especially useful when handling paginated data.</p> <p>See here for an example on how we use this in our projects at Nodes.</p> <p><em>NOTE:</em> responseSerializable Internally calls validate().responseJSON() on the request, so you don't have to do that.</p> <h3>Date parsing</h3> <p>Serpent can create Date objects from the date strings in the JSON. By default, Serpent can parse the date strings from the following formats: yyyy-MM-dd'T'HH:mm:ssZZZZZ , yyyy-MM-dd'T'HH:mm:ss , yyyy-MM-dd . If you need to parse other date formats, you can do it by adding this line to your code (for example, in AppDelegate 's didFinishLaunchingWithOptions: :</p> <pre> Date.customDateFormats = ["yyyyMMddHHmm", "yyyyMMdd"] // add the custom date formats you need here</pre> <p>The custom date formats won't replace the default ones, they will be still supported.</p> <h2>:busts_in_silhouette: Credits</h2> <p>Made with :heart: at <a href="/misc/goto?guid=4959742082528885017" rel="nofollow,noindex">Nodes</a> .</p> <h2>:page_facing_up: License</h2> <p>Serpentis available under the MIT license. See the <a href="/misc/goto?guid=4959742082615421869" rel="nofollow,noindex">LICENSE</a> file for more info.</p> <p> </p> <p> </p> <p> </p>