- Mapping JSON to objects
- Mapping objects to JSON
- Nested objects
- Custom transformations
The Gloss source currently available via CocoaPods, Carthage and Swift Package Manager. Example projects are included in this repository of integration (via CocoaPods and SPM) and sample implementation.
Gloss is compatible with Swift 5.0.
To use a version compatible with Swift 4.2, use version 2.1.x.
pod 'Gloss', '~> 3.1'
github "hkellaway/Gloss"
See Adding Package Dependencies to Your App. Search for Gloss
with Owner hkellaway
. Point to the desired version or the master
branch.
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.)
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 aguard
statement checking only non-Optional property(s)
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.)
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 thejsonify(_:)
function
(Note: If using custom operators like ~~>
is not desired, see On Not Using Gloss Operators.)
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)
}
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 can also be initialized directly from Data
for convenience:
let repoOwner: RepoOwner? = RepoOwner(data: repoOwnerData)
let repoOwners: [RepoOwner]? = [RepoOwner].from(data: repoOwnerDAta)
The JSON representation of an JSONEncodable
Gloss model is retrieved via toJSON()
:
repoOwner.toJSON()
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)
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
).
Gloss comes with a number of transformations built in for convenience (See: Gloss Operators).
NSDate
s 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)
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.
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 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)
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 ~~>
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:)
)
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.
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.
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! 💖
Check out Gloss in these cool places!
Gloss is available under the MIT license. See the LICENSE file for more info.