Why using pointers is important

Introduction

Lately at Capchase we’ve been working with the Stripe API. The use case can be easily simplified: we need to keep some client data on our side so that we can operate with it easier than fetching it every time. For this purpose we use Go, and with it comes all the marshaling and unmarshaling that we need to handle when working with an API.

Good to us, Stripe offers a handy Go package to work with the API without having to handle all the calls manually. To illustrate this, let’s consider that we want to get a specific coupon, for that we could code the following function

import (
    "github.com/stripe/stripe-go/v72"
    "github.com/stripe/stripe-go/v72/coupon"
)

// We consider that the initial API key setup has been done
// stripe.Key = "your_stripe_api_key"

// GetCoupon returns the Stripe coupon for the given id
func GetCoupon(id string) (*stripe.Coupon, error) {
    // Set wanted parameters...
    params := &stripe.CouponParams{}
    return coupon.Get(id, params)
}

Now, whenever we call cp, err := GetCoupon(someID) we will get a Stripe coupon object cp. Now, let’s have a look at the definition of this struct:

// Coupon is the resource representing a Stripe coupon.
// For more details see https://stripe.com/docs/api#coupons.
type Coupon struct {
    APIResource

    // Primitive fields
    AmountOff        int64             `json:"amount_off"`
    Created          int64             `json:"created"`
    Deleted          bool              `json:"deleted"`
    DurationInMonths int64             `json:"duration_in_months"`
    ID               string            `json:"id"`
    Livemode         bool              `json:"livemode"`
    MaxRedemptions   int64             `json:"max_redemptions"`
    Name             string            `json:"name"`
    Object           string            `json:"object"`
    PercentOff       float64           `json:"percent_off"`
    RedeemBy         int64             `json:"redeem_by"`
    TimesRedeemed    int64             `json:"times_redeemed"`
    Valid            bool              `json:"valid"`

    // Non-primitive fields
    AppliesTo        *CouponAppliesTo  `json:"applies_to"`
    Currency         Currency          `json:"currency"`
    Duration         CouponDuration    `json:"duration"`
    Metadata         map[string]string `json:"metadata"`
}

Note that I re-ordered the fields so that we have them divided into two groups, the first one with only primitive type fields, that is numerics, strings and booleans, and the following with the rest.

The problem

In particular, when working with Stripe coupons, only one of the fields AmountOff and PercentOff may be set. In fact in the official Stripe API docs we can find this example coupon object

{
  "id": "18OmM8HA",
  "object": "coupon",
  "amount_off": 25,
  "created": 1614605969,
  "currency": "usd",
  "duration": "repeating",
  "duration_in_months": 3,
  "livemode": false,
  "max_redemptions": 10,
  "metadata": {
    "test": "test"
  },
  "name": "25 off",
  "percent_off": null,
  "redeem_by": 1766448000,
  "times_redeemed": 0,
  "valid": true
}

Where percent_off is not set. The funny part comes when you marshal the Go coupon object, which would look something like

{
  "id": "18OmM8HA",
  "object": "coupon",
  "amount_off": 25,
  "created": 1614605969,
  "currency": "usd",
  "duration": "repeating",
  "duration_in_months": 3,
  "livemode": false,
  "max_redemptions": 10,
  "metadata": {
    "test": "test"
  },
  "name": "25 off",
  "percent_off": 0, 
  "redeem_by": 1766448000,
  "times_redeemed": 0,
  "valid": true
}

Why we get a 0 in the percent_off field?

If you have a closer look at the stripe.Coupon struct definition we see that the primitive fields are all non-pointer. Hence, we marshal the struct cp into bytes and look at it we get zero values for all those primitive types which were not set initially (PercentOff, for instance).

Solution

TL;DR: Use pointers 🥴

If we change the primitive fields to be pointers this problem is solved. As, when a field is unmarshaled from a null value, we will get a Go nil of the field type. This may be an issue because it forces the user to handle pointer logic. But one may use pointers always if the returned data can be nullable, because if not false data will eventually be served.

To illustrate, in a more simple example and without using any external package, consider the following code which can be seen in a playground:

package main

import (
    "encoding/json"
    "fmt"
)

type Drama struct {
    Primitive int  `json:"primitive"`
    Ptr       *int `json:"ptr"`
}

func main() {
    // Both fields empty
    d := Drama{}
    b, _ := json.MarshalIndent(d, "", "  ")
    fmt.Println(string(b))
}

The code outputs the following JSON string

{
  "primitive": 0,
  "ptr": null
}

The actual problem of this is that the end user of the Stripe package (or any other package that do not use pointers on nullable fields) it is literally impossible to know whether the field PercentOff was initially empty or not.

Postmortem

When we found the described issue, I immediately created an issue in the package repo, and after getting a better implement your own package response from one of the contributors we decided to fork…

After forking the package repo to our pointered version of stripe-go I spend a number of (not few) hours migrating every primitive field of every struct of stripe to be pointers and making the tests pass… Which end up being much harder than expected because of a hidden forgotten channel in one of the tests (I may talk about that another day).

Small commit

Finally, we deployed our own version and started using it, which yielded to the wanted results!

If you liked this post ping me on Twitter and… We’re hiring! Have a look the open positions at Capchase here.