JSON From Swift Types
<!DOCTYPE html> JSON From Swift Types

JSON From Swift Types

When you end up dealing with nested data types, you’re only ε away from converting those data types to JSON1. We can pretty easily add some data to our custom types allowing them to automatically be serialized to JSON.

The Data Model

We’re going to stay in Stripe Data Model Land for our JSON conversion, but we’re going to use a new StripeMiniCard type with only five properties defined. The reduced property count will make following along easier.

The Scaffolding

The one trick to make the entire to-JSON conversion process work is one tiny protocol:

1
2
3
4
protocol ReadableAsJson {
    func jsonName() -> String
    func jsonValue() -> String
}

The protocol ReadableAsJson says: any type implementing the protocol must define two methods. jsonName returns the name of the field for the type and jsonValue returns the value of the field.

The Data Type Implementations

First, we add ReadableAsJson to our base class StripeIdentifier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StripeIdentifier: ReadableAsJson {
    let id: String
    init(prefixes: Array<String>, id: String) {
        /* Check if id starts with any prefix in prefixes */
        let matched = prefixes.filter { id.hasPrefix($0) }
        
        if !matched.isEmpty {
            /* If a prefix matched,
            * (meaning our result set isn't empty), set our id */
            self.id = id
        } else {
            /* If our prefix search failed, set empty id */
            self.id = ""
        }
    }
    func jsonName() -> String {
        return "nokey"
    }
    func jsonValue() -> String {
        return id
    }
}

Since every subclass of StripeIdentifier stores data in id, subclasses only need to override the jsonName function to set their proper field name.

Speaking of identifiers, let’s jump into StripeCardId:

1
2
3
4
5
6
7
8
class StripeCardId: StripeIdentifier {
    init(id: String) {
        super.init(prefixes: ["card_"], id: id)
    }
    override func jsonName() -> String {
        return "card_id"
    }
}

Note the use of the keyword override when we actually override a parent class function in a subclass.

Now let’s try some enums:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
enum StripeCheckResult: String, ReadableAsJson {
    case Pass = "pass"
    case Fail = "fail"
    case Unchecked = "unchecked"
    func jsonName() -> String {
        return "cvc_check"
    }
    func jsonValue() -> String {
        return self.toRaw()
    }
}

enum StripeCardBrand: String, ReadableAsJson {
    case Visa = "Visa"
    case Amex = "American Express"
    case MasterCard = "MasterCard"
    case Discover = "Discover"
    case JCB = "JCB"
    case DinersClub = "Diners Club"
    case Unknown = "Unknown"
    func jsonName() -> String {
        return "type"
    }
    func jsonValue() -> String {
        return self.toRaw()
    }
}

Notice how each enum has default raw value strings. We can just return toRaw() as our JSON value.

Moving on to two quick structs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct StripeFingerprint: ReadableAsJson {
    let fingerprint: String
    func jsonName() -> String {
        return "fingerprint"
    }
    func jsonValue() -> String {
        return fingerprint
    }
}

struct StripeLast4: ReadableAsJson {
    let digits: Int
    init(digits: String) {
        self.init(digits: digits.toInt()!)
    }
    
    init(digits: Int) {
        if digits > 0 || digits < 10000 {
            self.digits = digits
        } else {
            self.digits = 0
        }
    }
    func jsonName() -> String {
        return "last4"
    }
    func jsonValue() -> String {
        return String(format: "%d", digits)
    }
}

Pretty simple. Each struct provides a name and JSON string value so they conform to the ReadableAsJson protocol.

Now let’s try an enum with no default raw values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum StripeObjectType: ReadableAsJson {
    case Charge, Card, Customer, Invoice, InvoiceItem, Plan, Token
    func jsonName() -> String {
        return "object"
    }
    func jsonValue() -> String {
        switch self {
        case .Charge: return "charge"
        case .Card: return "card"
        case .Customer: return "customer"
        case .Invoice: return "invoice"
        case .InvoiceItem: return "invoice_item"
        case .Plan: return "plan"
        case .Token: return "token"
        }
    }
}

Since StripeObjectType had no raw values, we switch on ourself and return the proper String. The decision to put the values in a switch inside jsonValue is a pure design choice. We could have just as easily set the strings as default raw values like we did with StripeCardBrand or StripeCheckResult.

Creating JSON From Instance Properties

All property types have been conformed to our ReadableAsJson protocol, so it’s time to tie it all together.

First, we import Foundation so we can access the Cocoa JSON serializer:

1
import Foundation

The NSJSONSerialization class provides a very simple way to generate JSON from a Swift dictionary type. All we need to do now is iterate over all properties of our StripeMiniCard, read JSON field name and value, then store the name as a key in our Swift dictionary along with it’s value.

After the dictionary is created, we can call NSJSONSerialization.dataWithJSONObject() to convert our dictionary to JSON.

Let’s define our StripeMiniCard with toJson() capability:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct StripeMiniCard {
    let id: StripeCardId
    let object = StripeObjectType.Card
    let last4: StripeLast4
    let type: StripeCardBrand
    let fingerprint: StripeFingerprint
    
    init(id: String, last4: String, type: String, fingerprint: String) {
            self.id = StripeCardId(id: id)
            self.last4 = StripeLast4(digits: last4)
            self.type = StripeCardBrand.fromRaw(type)!
            self.fingerprint = StripeFingerprint(fingerprint: fingerprint)
    }
    
    func toJson() -> String {
        var build = Dictionary<String, String>()
        var evaluate = Array<ReadableAsJson>()
        evaluate = [id, object, last4, type, fingerprint]
        
        for jsonCapable in evaluate {
            /* for each of our custom types, add their json data to our dict */
            build[jsonCapable.jsonName()] = jsonCapable.jsonValue()
        }
        
        var jsonData = NSJSONSerialization.dataWithJSONObject(build,
            options: nil, error: nil)
        return NSString(data: jsonData, encoding:NSUTF8StringEncoding)
    }
}

Notice how the array evaluate is defined as an array of ReadableAsJson. We don’t care about the actual type of our array elements. All we care about is if each element implements our two JSON reading requirements.

After we populate our dictionary, we use built in libraries to convert the dictionary to JSON. But, the library returns an annoying NSData, so we need to convert the NSData back to an acceptable String type (NSString bridges to Swift String, so there’s no casting or manual conversion required).

So what’s the final result?

1
2
3
4
5
let card = StripeMiniCard(id: "card_104BZB2eZvKYlo2CRnmgsNrD",
    last4: "4242", type: "Visa",
    fingerprint: "Xt5EWLLDS7FJjR1c")

println(card.toJson())

From those tiny lines card.toJson() printed:

1
2
{"object":"card","last4":"4242","type":"Visa",
"card_id":"card_104BZB2eZvKYlo2CRnmgsNrD","fingerprint":"Xt5EWLLDS7FJjR1c"}

…which is pretty darn good considering we just made up our entire data retrieval and JSON creation scheme out of nowhere.

Obviously the actual StripeCard type has many more fields, but populating more fields using this method is just a matter of time and effort.

Also note how, by using custom types even for something simple like encapsulating only one String (see: StripeFingerprint), we are able to attach more metadata to our types than if we used raw String or Int types in isolation.


  1. Plus, everybody knows if you’re a company, and you add JSON to your feature checklist, the company is immediately becomes worth $200 to $800 million more than before you added those four simple letters to your feature checklist.