/Gloss

A shiny JSON parsing library in Swift :sparkles:

Primary LanguageSwiftMIT LicenseMIT

Gloss

Features ✨

CocoaPods Carthage compatible SPM License CocoaPods Reference Status Build Status

  • Mapping JSON to objects
  • Mapping objects to JSON
  • Nested objects
  • Custom transformations

Getting Started

  • Download Gloss and do a pod install on the included GlossExample app to see Gloss in action
  • Check out the documentation for a more comprehensive look at the classes available in Gloss

Swift 2.3 and Swift 3.0

Use the swift_2.3 and swift_3.0 branches for compatible versions of Gloss plus Example project that are compatible with Swift 2.3 and Swift 3.0 respectively.

The Gloss source currently available on CocoaPods and Carthage is compatible with Swift 2.3.

Installation with CocoaPods

pod 'Gloss', '~> 0.7'

Installation with Carthage

github "hkellaway/Gloss"

Installation with Swift Package Manager

To use Gloss as a Swift Package Manager package just add the following in your Package.swift file.

import PackageDescription

let package = Package(
    name: "HelloWorld",
    dependencies: [
        .Package(url: "https://github.com/hkellaway/Gloss.git", majorVersion: 0)
    ]
)

Usage

Deserialization

A Simple Model

Let's imagine we have a simple model represented by the following JSON:

{
  "id" : 5456481,
  "login" : "hkellaway"
}

Our Gloss model would look as such:

import Gloss

struct RepoOwner: Decodable {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization

    init?(json: JSON) {
        self.ownerId = "id" <~~ json
        self.username = "login" <~~ json
    }

}

This model:

  • Imports Gloss
  • Adopts the Decodable protocol
  • Implements the init?(json:) initializer

(Note: If using custom operators like <~~ is not desired, see On Not Using Gloss Operators.)

A Simple Model with Non-Optional Properties

The prior example depicted the model with only Optional properties - i.e. all properties end with a ?. If you are certain that the JSON being used to create your models will always have the values for your properties, you can represent those properties as non-Optional.

Non-Optional properties require additional use of the guard statement within init?(json:) to make sure the values are available at runtime. If values are unavailable, nil should be returned.

Let's imagine we know that the value for our RepoOwner property ownerId will always be available:

import Gloss

struct RepoOwner: Decodable {

    let ownerId: Int
    let username: String?

    // MARK: - Deserialization

    init?(json: JSON) {
        guard let ownerId: Int = "id" <~~ json
            else { return nil }

        self.ownerId = ownerId
        self.username = "login" <~~ json
    }
}

This model has changed in two ways:

  • The ownerId property is no longer an Optional
  • The init?(json:) initializer now has a guard statement checking only non-Optional property(s)

A More Complex Model

Let's imagine we had a more complex model represented by the following JSON:

{
	"id" : 40102424,
	"name": "Gloss",
	"description" : "A shiny JSON parsing library in Swift",
	"html_url" : "https://github.com/hkellaway/Gloss",
	"owner" : {
		"id" : 5456481,
		"login" : "hkellaway"
	},
	"language" : "Swift"
}

This model is more complex for a couple reasons:

  • Its properties are not just simple types
  • It has a nested model, owner

Our Gloss model would look as such:

import Gloss

struct Repo: Decodable {

    let repoId: Int?
    let name: String?
    let desc: String?
    let url: NSURL?
    let owner: RepoOwner?
    let primaryLanguage: Language?

    enum Language: String {
        case Swift = "Swift"
        case ObjectiveC = "Objective-C"
    }

    // MARK: - Deserialization

    init?(json: JSON) {
        self.repoId = "id" <~~ json
        self.name = "name" <~~ json
        self.desc = "description" <~~ json
        self.url = "html_url" <~~ json
        self.owner = "owner" <~~ json
        self.primaryLanguage = "language" <~~ json
    }

}

Despite being more complex, this model is just as simple to compose - common types such as an NSURL, an enum value, and another Gloss model, RepoOwner, are handled without extra overhead! 🎉

Serialization

Next, how would we allow models to be translated to JSON? Let's take a look again at the RepoOwner model:

import Gloss

struct RepoOwner: Glossy {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization
    // ...

    // MARK: - Serialization

    func toJSON() -> JSON? {
        return jsonify([
            "id" ~~> self.ownerId,
            "login" ~~> self.username
        ])
    }

}

This model now:

  • Adopts the Glossy protocol
  • Implements toJSON() which calls the jsonify(_:) function

(Note: If using custom operators like ~~> is not desired, see On Not Using Gloss Operators.)

Initializing Model Objects and Arrays

Instances of Decodable Gloss models are made by calling init?(json:).

For example, we can create a RepoOwner as follows:

let repoOwnerJSON = [
        "id" : 5456481,
        "name": "hkellaway"
]

guard let repoOwner = RepoOwner(json: repoOwnerJSON)
    else { /* handle nil object here */ }

print(repoOwner.repoId)
print(repoOwner.name)

Or, using if let syntax:

if let repoOwner = RepoOwner(json: repoOwnerJSON) {
    print(repoOwner.repoId)
    print(repoOwner.name)
}

Model Objects from JSON Arrays

Gloss also supports creating models from JSON arrays. The fromJSONArray(_:) function can be called on a Gloss model array type to produce an array of objects of that type from a JSON array passed in.

For example, let's consider the following array of JSON representing repo owners:

let repoOwnersJSON = [
    [
        "id" : 5456481,
        "name": "hkellaway"
    ],
    [
        "id" : 1234567,
        "name": "user2"
    ]
]

An array of RepoOwner objects could be obtained via the following:

guard let repoOwners = [RepoOwner].fromJSONArray(repoOwnersJSON) else {
    // handle decoding failure here
}

print(repoOwners)

Translating Model Objects to JSON

The JSON representation of an Encodable Gloss model is retrieved via toJSON():

repoOwner.toJSON()

JSON Arrays from Model Objects

An array of JSON from an array of Encodable models is retrieved via toJSONArray():

guard let jsonArray = repoOwners.toJSONArray() else {
    // handle encoding failure here
}

print(jsonArray)

Additonal Topics

Gloss Transformations

Gloss comes with a number of transformations built in for convenience (See: Gloss Operators).

Date Transformations

NSDates require an additional dateFormatter parameter, and thus cannot be retrieved via binary operators (<~~ and ~~>).

Translating from and to JSON is handled via:

Decoder.decodeDate(key:, dateFormatter:) and Decode.decodeDateArray(key:, dateFormatter:) where key is the JSON key and dateFormatter is the NSDateFormatter used to translate the date(s). e.g. self.date = Decoder.decodateDate("dateKey", dateFormatter: myDateFormatter)(json)

Encoder.encodeDate(key:, dateFormatter:) and Encode.encodeDate(key:, dateFormatter:) where key is the JSON key and dateFormatter is the NSDateFormatter used to translate the date(s). e.g. Encoder.encodeDate("dateKey", dateFormatter: myDateFormatter)(self.date)

Custom Transformations

From JSON

You can write your own functions to enact custom transformations during model creation.

Let's imagine the username property on our RepoOwner model was to be an uppercase string. We could update as follows:

import Gloss

struct RepoOwner: Decodable {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization

    init?(json: JSON) {
        self.ownerId = "id" <~~ json
        self.username = Decoder.decodeStringUppercase("login", json: json)
    }

}

extension Decoder {

    static func decodeStringUppercase(key: String, json: JSON) -> String? {
            
        if let string = json.valueForKeyPath(key) as? String {
            return string.uppercaseString
        }

        return nil
    }

}

We've created an extension on Decoder and written our own decode function, decodeStringUppercase.

What's important to note is that the return type for decodeStringUppercase is the desired type -- in this case, String?. The value you're working with will be accessible via json.valueForKeyPath(_:) and will need to be cast to the desired type using as?. Then, manipulation can be done - for example, uppercasing. The transformed value should be returned; in the case that the cast failed, nil should be returned.

Though depicted here as being in the same file, the Decoder extension is not required to be. Additionally, representing the custom decoding function as a member of Decoder is not required, but simply stays true to the semantics of Gloss.

To JSON

You can also write your own functions to enact custom transformations during JSON translation.

Let's imagine the username property on our RepoOwner model was to be a lowercase string. We could update as follows:

import Gloss

struct RepoOwner: Glossy {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization
    // ...

   // MARK: - Serialization

    func toJSON() -> JSON? {
        return jsonify([
            "id" ~~> self.ownerId,
            Encoder.encodeStringLowercase("login", value: self.username)
        ])
    }


}

extension Encoder {

    static func encodeStringLowercase(key: String, value: String?) -> JSON? {
            
        if let value = value {
            return [key : value.lowercaseString]
        }

        return nil
    }

}

We've created an extension on Encoder and written our own encode function, encodeStringLowercase.

What's important to note is that encodeStringLowercase takes in a value whose type is what it's translating from (String?) and returns JSON?. The value you're working with will be accessible via the if let statement. Then, manipulation can be done - for example, lowercasing. What should be returned is a dictionary with key as the key and the manipulated value as its value. In the case that the if let failed, nil should be returned.

Though depicted here as being in the same file, the Encoder extension is not required to be. Additionally, representing the custom encoding function as a member of Encoder is not required, but simply stays true to the semantics of Gloss.

Gloss Operators

On Not Using Gloss Operators

Gloss offers custom operators as a way to make your models less visually cluttered. However, some choose not to use custom operators for good reason - custom operators do not always clearly communicate what they are doing (See this discussion).

If you wish to not use the <~~ or ~~> operators, their Decoder.decode and Encoder.encode complements can be used instead.

For example,

self.url = "html_url" <~~ json would become self.url = Decoder.decodeURL("html_url")(json)

and

"html_url" ~~> self.url would become Encoder.encodeURL("html_url")(self.url)

On Using Gloss Operators

The Decode Operator: <~~

The <~~ operator is simply syntactic sugar for a set of Decoder.decode functions:

  • Simple types (Decoder.decode)
  • Decodable models (Decoder.decodeDecodable)
  • Simple arrays (Decoder.decode)
  • Arrays of Decodable models (Decoder.decodeDecodableArray)
  • Dictionaries of Decodable models (Decoder.decodeDecodableDictionary)
  • Enum types (Decoder.decodeEnum)
  • Enum arrays (Decoder.decodeEnumArray)
  • Int32 types (Decoder.decodeInt32)
  • Int32 arrays (Decoder.decodeInt32Array)
  • UInt32 types (Decoder.decodeUInt32)
  • UInt32 arrays (Decoder.decodeUInt32Array)
  • Int64 types (Decoder.decodeInt64)
  • Int64 array (Decoder.decodeInt64Array)
  • UInt64 types (Decoder.decodeUInt64)
  • UInt64 array (Decoder.decodeUInt64Array)
  • NSURL types (Decoder.decodeURL)
  • NSURL arrays (Decode.decodeURLArray)
The Encode Operator: ~~>

The ~~> operator is simply syntactic sugar for a set of Encoder.encode functions:

  • Simple types (Encoder.encode)
  • Encodable models (Encoder.encodeEncodable)
  • Simple arrays (Encoder.encodeArray)
  • Arrays of Encodable models (Encoder.encodeEncodableArray)
  • Dictionaries of Encodable models (Encoder.encodeEncodableDictionary)
  • Enum types (Encoder.encodeEnum)
  • Enum arrays (Encoder.encodeEnumArray)
  • Int32 types (Encoder.encodeInt32)
  • Int32 arrays (Encoder.encodeInt32Array)
  • UInt32 types (Encoder.encodeUInt32)
  • UInt32 arrays (Encoder.encodeUInt32Array)
  • Int64 types (Encoder.encodeInt64)
  • Int64 arrays (Encoder.encodeInt64Array)
  • UInt64 types (Encoder.encodeUInt64)
  • UInt64 arrays (Encoder.encodeUInt64Array)
  • NSURL types (Encoder.encodeURL)

Gloss Protocols

Models that are to be created from JSON must adopt the Decodable protocol.

Models that are to be transformed to JSON must adopt the Encodable protocol.

The Glossy protocol depicted in the examples is simply a convenience for defining models that can translated to and from JSON. Glossy can be replaced by Decodable, Encodable for more preciseness, if desired.

Why "Gloss"?

The name for Gloss was inspired by the name for a popular Objective-C library, Mantle - both names are a play on the word "layer", in reference to their role in supporting the model layer of the application.

The particular word "gloss" was chosen as it evokes both being lightweight and adding beauty.

Credits

Gloss was created by Harlan Kellaway.

Inspiration was gathered from other great JSON parsing libraries like Argo. Read more about why Gloss was made here.

Special thanks to all contributors! 💖

Featured

Check out Gloss in these cool places!

Posts

Libraries

SDKs/Products

Apps

Tools

Newsletters

Using Gloss in your app? [Let me know.](mailto:hello@harlankellaway.com?subject=Using Gloss in my app)

License

Gloss is available under the MIT license. See the LICENSE file for more info.