/Gloss

A shiny JSON parsing library in Swift :sparkles:

Primary LanguageSwiftMIT LicenseMIT

Gloss

Features ✨

CocoaPods Carthage compatible SPM License CocoaPods 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 Version

The Gloss source currently available via CocoaPods and Carthage is compatible with Swift 5.0.

To use a version compatible with Swift 4.2, use version 2.1.x.

Installation with CocoaPods

pod 'Gloss', '~> 3.0'

Installation with Carthage

github "hkellaway/Gloss"

Installation with Swift Package Manager

import PackageDescription

let package = Package(
    name: "HelloWorld",
    dependencies: [
        .Package(url: "https://github.com/hkellaway/Gloss.git", majorVersion: 3, minor: 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: JSONDecodable {

    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 JSONDecodable 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: JSONDecodable {

    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: JSONDecodable {

    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! 🎉

-(Note: If nested models are present in JSON but not desired in your Gloss models, see Retrieving Nested Model Values without Creating Extra Models.)

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 JSONDecodable 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 decoding failure 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 from(jsonArray:) 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].from(jsonArray: repoOwnersJSON) else {
    // handle decoding failure here
}

print(repoOwners)

Model Objects from Data

Model objects can also be initialized directly from Data for convenience:

let repoOwner: RepoOwner? = RepoOwner(data: repoOwnerData)
let repoOwners: [RepoOwner]? = [RepoOwner].from(data: repoOwnerDAta)

Translating Model Objects to JSON

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

repoOwner.toJSON()

JSON Arrays from Model Objects

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

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

print(jsonArray)

Retrieving Nested Model Values without Creating Extra Models

We saw in earlier examples that Repo has a nested model RepoOwner - and that nested Gloss models are handled automatically. But what if the nested models represented in our JSON really don't need to be their own models?

Gloss provides a way to indicate nested model values with simple . syntax - let's revisit the owner values for Repo and see what changes:

import Gloss

struct Repo: Glossy {

    let ownerId: Int?
    let ownerUsername: String?

    // MARK: - Deserialization

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

    // MARK: - Serialization

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

}

Now, instead of declaring a nested model owner of type RepoOwner with its own id and username properties, the desired values from owner are retrieved by specifying the key names in a string delimited by periods (i.e. owner.id and owner.login).

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.decode(dateForKey:, dateFormatter:) and Decode.decode(dateArrayFromKey:, dateFormatter:) where key is the JSON key and dateFormatter is the DateFormatter used to translate the date(s). e.g. self.date = Decoder.decode(dateForKey: "dateKey", dateFormatter: myDateFormatter)(json)

Encoder.encode(dateForKey:, dateFormatter:) and Encode.encode(dateForKey:, dateFormatter:) where key is the JSON key and dateFormatter is the DateFormatter used to translate the date(s). e.g. Encoder.encode(dateForKey: "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: JSONDecodable {

    let ownerId: Int?
    let username: String?

    // MARK: - Deserialization

    init?(json: JSON) {
        self.ownerId = "id" <~~ json
        self.username = Decoder.decodeStringUppercase(key: "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(key: "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(key:))
  • JSONDecodable models (Decoder.decode(decodableForKey:))
  • Simple arrays (Decoder.decode(key:))
  • Arrays of JSONDecodable models (Decoder.decode(decodableArrayForKey:))
  • Dictionaries of JSONDecodable models (Decoder.decode(decodableDictionaryForKey:))
  • Enum types (Decoder.decode(enumForKey:))
  • Enum arrays (Decoder.decode(enumArrayForKey:))
  • Int32 types (Decoder.decode(int32ForKey:))
  • Int32 arrays (Decoder.decode(int32ArrayForKey:))
  • UInt32 types (Decoder.decode(uint32ForKey:))
  • UInt32 arrays (Decoder.decode(uint32ArrayForKey:))
  • Int64 types (Decoder.decode(int64ForKey:))
  • Int64 array (Decoder.decode(int64ArrayForKey:))
  • UInt64 types (Decoder.decode(uint64ForKey:))
  • UInt64 array (Decoder.decode(uint64ArrayForKey:))
  • Double types (Decoder.decode(doubleForKey:))
  • Double array (Decoder.decode(doubleArrayForKey:))
  • NSURL types (Decoder.decode(urlForKey:))
  • NSURL arrays (Decode.decode(urlArrayForKey:))
  • UUID types (Decoder.decode(uuidForKey:))
  • UUID arrays (Decoder.decode(uuidArrayForKey:))
  • Decimal types (Decoder.decode(dedimalForKey:))
  • Decimal arrays (Decoder.decode(decimalArrayForKey:))
The Encode Operator: ~~>

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

  • Simple types (Encoder.encode(key:))
  • JSONEncodable models (Encoder.encode(encodableForKey:))
  • Simple arrays (Encoder.encode(arrayForKey:))
  • Arrays of JSONEncodable models (Encoder.encode(encodableArrayForKey:))
  • Dictionaries of JSONEncodable models (Encoder.encode(encodableDictionaryForKey:))
  • Enum types (Encoder.encode(enumForKey:))
  • Enum arrays (Encoder.encode(enumArrayForKey:))
  • Int32 types (Encoder.encode(int32ForKey:))
  • Int32 arrays (Encoder.encode(int32ArrayForKey:))
  • UInt32 types (Encoder.encode(uint32ForKey:))
  • UInt32 arrays (Encoder.encode(uint32ArrayForKey:))
  • Int64 types (Encoder.encode(int64ForKey:))
  • Int64 arrays (Encoder.encode(int64ArrayForKey:))
  • UInt64 types (Encoder.encode(uint64ForKey:))
  • UInt64 arrays (Encoder.encode(uint64ArrayForKey:))
  • Double types (Encoder.encode(doubleForKey:))
  • Double arrays (Encoder.encode(doubleArrayForKey:))
  • NSURL types (Encoder.encode(urlForKey:))
  • UUID types (Encoder.encode(uuidForKey:))
  • Decimal types (Encoder.encode(decimalForKey:))
  • Decimal array (Encoder.encode(decimalArrayForKey:))

Gloss Protocols

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

Models that are to be transformed to JSON must adopt the JSONEncodable 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 JSONDecodable, JSONEncodable 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

Newsletters

License

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