Real World Example: Stripe Data Types in Swift
Let’s practice using Swift to create data models for a real-world API.
One of the most important places to have correct code in your application is around areas of payment processing.
We can use our new-found knowledge of implementing custom types with Swift containers to model the Stripe API in Swift as safely as possible.
If you’re not familiar with Stripe, their product is an online payment API used by developers to easily accept credit cards online and create online peer-to-peer payment marketplaces.
The Stripe API
Let’s dive in to everybody’s favorite Stripe API object: charge
.
The Charge Object
The charge
object looks like this:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Notice the charge
object has an embedded card
object and the embedded card
object is using a super credit card with expiration date 34 years in the future. The charge
object gives amount
in units of cents for currency
and includes various identifiers for referencing these objects in future Stripe API calls.
Also notice many fields are null
which means we can use Swift optionals to represent the potential absence of values for those fields.
Every Stripe object has a unique object id and the type of the object is conveniently embedded as a prefix of the string object id. For example, the field card.id
is prefixed with “card”
and the field id
for the entire card
object is prefixed with “ch”
letting us know the id refers to a charge
object.
Let’s validate every id prefix, along with the very blatant object
field, to make sure we’re reading the correct kinds of objects.
We’ll start by defining the types needed to create both the inner card
object as well as the primary charge
object.
Modeling the Charge Object
We see the card
object has these types of things inside of it:
-
id for the
charge
itself - object name
- created epoch timestamp
- booleans about if this charge was test vs. live, whether the payment is complete, if this charge wasn’t a charge at all but rather a refund, and if the charge has gone through payment processing yet
-
an amount in cents in
currency
- three letter lower case ISO currency abbreviation
-
embedded
card
object with- credit card required details
- credit card optional details (address informations mostly)
- validity of credit card
-
many
null
’d out elements
- list of potential partial refunds for this charge
- …and a few more things.
Let’s be as specific as possible when creating our Swift-based StripeCard
type and create all types the StripeCard
type will reference.
Modeling IDs
First up, the charge id. We see the charge id is prefixed with ch
, so we can create a custom StripeChargeId
type to only store properly formatted charge ids.
1 2 3 4 5 6 7 8 9 10 |
|
When we create a StripeChargeId
, the value will either be a properly prefixed id or the empty string. Using an empty string for invalid input is a design decision and we could just as easily have defined id
as String?
to allow id to be nil
. For now, let’s just populate using either a known good value or a known empty value.
We have a few different id-type objects to create though. Let’s make a Swift protocol to ensure all our types respond to the same requests for identification.
1 2 3 |
|
The StripeId
protocol just says: anything implementing StripeId
must have a property named id
we can read.
Now let’s update StripeChargeId
to use our new protocol:
1 2 3 4 5 6 7 8 9 10 |
|
That’s all it takes. Protocols look like type information or parent types of classes, but Swift knows what to do with each type of type in the type field.
Moving on, we have more id types to create:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
This is starting to look a little too copy/pasty. All we’re changing in the structs are the type names and the prefix conditions.
Refactoring IDs
Can we centralize some of our assignment functions? Structs can’t inherit functionality, but classes can. Let’s use some inheritance!
First, let’s create a class to represent the concept of a Stripe id :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
This generic StripeIdentifier
class is more verbose than our previous individual id structs, but it still conforms to protocol StripeId
.
Now we can implement individual id types by simply:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
When instantiating a type, we pass in a list of valid prefixes for the type. If id
doesn’t match any of our acceptable prefixes, we default id
to the empty string.
Now let’s make even more types!
What’s left in the charge
object? We have a timestamp, some booleans, some money values, a currency, an embedded card
object, and a bunch of null
values referencing other ids or types we didn’t get returned.
Let’s start with the timestamp:
1 2 3 |
|
Simple enough. Since the timestamp isn’t an identifier, we’re not asking StripeTimestamp
to conform to protocol StripeId
.
Modeling Booleans
Next up, some raw booleans expanded to more meaningful types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
How do we convert the card
object’s livemode
value of false
into a value of StripeMode.Test
?
We can create a custom initializer for the enum to convert the Stripe-provided boolean into our proper type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Now, on to creating an initializer for StripePaid
too:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Let’s finish these fields out by adding initializers to StripeRefunded
and StripeCardCaptured
:
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 |
|
It does look a little copy/pasty, but that’s okay for now. We have a limited number of these very specific boolean types.
Let’s move on to money values.
Modeling Money
Money, money, money. Money is better when it’s statically typed.
Stripe gives us amount
as an integer number of cents. We can do better than a raw, typeless, number.
Let’s start by defining a protocol for Stripe money types:
1 2 3 |
|
The protocol just says any StripeMoney
will have a cents
property we can read.
Stripe has two kinds of money values:
- prices of items you sell, which can range between 50 cents and $999,999.99
- money amounts returned from Stripe representing either money in your account or some other Stripe-created money value with no limits
For price amounts, we want to restrict the range to between [50, 99999999] cents. For Stripe-given money amounts, we want to accept any non-negative integer.
Let’s define our Stripe money types as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Now we are guaranteed a StripePrice
will never be invalid (though, we are setting to 0
upon invalid input; this would cause an error on the Stripe API, but a known, non-dangerous error). We are also guaranteed we will never have negative StripeAmount
values to surprise our accounting math.
Implementation note: Stripe does support some currencies without any concept of “cents” (i.e. they don’t have fractional units of currency). For those currencies, you use whole units instead of fractional pieces.
Exercise: How would you fit a currency not having cents into our StripeMoney
data model?
Modeling Worldwide Currencies
The sample card
object we’re working from has a currency
value of “usd”
.
What other currencies are available? It turns out Stripe currently supports 139 currencies.
If you’re thinking ahead, yes, we’re going to implement a 139 property enum.
I’m not going to paste the entire implementation here since it’s close to 300 lines1, but here’s a taste of how our currency enum could work:
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 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Okay, what just happened there?
If you refer back to our charge
object, currency
is a string: three lower case letters denoting the currency.
We can use the Stripe-provided abbreviation to populate our correct StripeCurrency
enum value:
1 |
|
Remember from our enum introduction: fromRaw
does not return the type directly, but rather it returns an optional. Swift has no way of knowing we will only pass in known-good values. Swift reserves the right to return nil
if we pass in a non-matching raw string like “princess bubblegum is evil”
when asking for StripeCurrency
.
To use the returned currency value, we should check to make sure we did get a currency back and not just nil
:
1 2 3 4 5 6 7 8 |
|
Note how we unwrap the optional by using !
only after we are sure the optional is not nil
.
Okay, so that covers the default raw strings on the enum. What’s up with the desc()
function? The desc()
function provides an in-enum helpful lookup table so we can get the actual currency name back again once we have an enum value:
1 |
|
Note also how desc().currency
holds the same value as our original enum instance, but it is not the same enum instance:
1 |
|
In Swift, ==
tests for equality of value while ===
tests if two instances refer to the exact same storage location. Since enums (and structs) are value types, they have no ability to be compared by ===
since nothing could ever be equal to their copy-on-assignment storage location.
Modeling Embedded Objects
As we saw previously, properties of any Swift struct or class can be defined to take on any type available in Swift.
charge
contains an entire sub-object in the card
field. Let’s create types required by the card
object so we can then define a card
type:
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 31 32 33 34 35 36 37 38 |
|
Note how in StripeCardBrand
and StripeCheckResult
we set default raw values on each enum property to be exactly the same string returned in the Stripe object. By using Stripe return values as our default enum values, we can use fromRaw()
reliably to instantiate the correct enum value.
Now let’s define StripeCard
in terms of our just-defined types (we’ll use raw Swift types for some values for expediency here):
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Swift requires us to define every property at initialization time, but we can skip defining properties marked optional since it’s okay if they have a default value of nil
.
If we give a property a default value (like object
), we do not have to set the property in the initializer since it already has a default value.
Note how for these Stripe types, every property is marked let
. These types are for representing objects returned from the Stripe API. Once the object is created, we don’t want to change any properties because every property represents state on the Stripe API. If we need updated values, we should re-fetch the object from the API.
Finally Creating the charge
Object
By now we’ve defined enough sub-types of the charge
object, so we can use all our clever types to create an unbreakable StripeCharge
Swift type.
Now, finally, let’s create our StripeCharge
type from mostly our custom types:
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 31 32 33 34 35 36 37 38 |
|
Now, with our StripeCard
and our StripeCharge
types, we can finally create an instance of StripeCharge
from the original charge
object data.
First, we create the inner card
instance:
1 2 3 4 |
|
Then, we can create our charge
instance:
1 2 3 4 5 6 |
|
And that, dear reader, is how you can strictly model important data types inside Swift.
For the StripeCharge
object we did take some shortcuts. We didn’t expose every property to init
. There are a few types we skipped over defining such as the Dispute
and Refund
types. Since we didn’t expose every property as parameters to our init
method, any unset (or default) property will always be fixed to their default values. For a proper production implementation, your initialization must take every property as a parameter.
Extra Types
For more practice, we can define a few more Stripe objects almost completely from the types we’ve already implemented.
Here’s an attempt at a partial StripeCustomer
with new StripeEmail
and StripeCustomerId
types:
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 31 32 33 |
|
Note how in this case StripeCustomerId
has two potential prefixes, so we add both to the prefixes
array.
The Remaining 15 Types
Don’t worry, we’re not going to implement the remaining 15 Stripe API object types.
If you want more practice thinking about and implementing very specific types in Swift, implement Stripe’s Subscription and Invoice objects as types inside Swift too.
-
The full implementation is in the Xcode Swift Playground for this chapter.↩