Shopify Value Objects for Go /
An existing project had grown rapidly, resulting in many places where IDs were manually extracted from GIDs or formatted into GIDs without validation and passing the raw values around. To address this, I developed a small internal package with the following goals:
- Provide a common object for GID types such as Customer, Order, Variant, and Product
- Validate GID formatting, ensure the GID type matches expectations, and confirm extracted IDs are valid
- Support conversions between IDs and GIDs in both directions
- Enable equality checks between objects
- Allow for marshalling and unmarshalling
Implementation
To start off, I created an interface for the objects and a list of available types
package gid
// ...
// Identifiers represents a set of GID types.
type Identifiers interface {
| ProductID | VariantID | OrderID | InventoryItem
CustomerID }
// Identifier represents a GID.
type Identifier interface {
() int
ID() string
String(ogid Identifier) bool
Equal() bool
IsValid}
// ...
Now, Identifier
represents a GID and has the following methods:
-
ID()
to return the ID inside the GID -
String()
to return the GID string containing the type and ID -
Equal()
to perform equality checks on two objects -
IsValid()
to ensure object is valid
Next, added methods to support bulk conversions of GIDs to IDs or vise-verse:
package go
// ...
// ToIDs converts GIDs to their int representation.
func ToIDs[T Identifiers](gids []T) []int {
:= make([]int, len(gids))
ids for i, gid := range gids {
[i] = Identifier(gid).ID()
ids}
return ids
}
// ToStrings converts GIDs to their string representation.
func ToStrings[T Identifiers](gids []T) []string {
:= make([]string, len(gids))
strs for i, gid := range gids {
[i] = Identifier(gid).String()
strs}
return strs
}
// ...
With the foundation established, I started to implement creation methods for the value objects.
package gid
// ...
// typeFrom returns the type name for a given GID type.
func typeFrom[T Identifiers]() string {
switch any(*new(T)).(type) {
case CustomerID:
return "Customer"
case ProductID:
return "Product"
case VariantID:
return "ProductVariant"
case OrderID:
return "Order"
case InventoryItem:
return "InventoryItem"
default:
return ""
}
}
// commonNew creates a new GID from a value.
// It supports int64, int, and string formats.
// String formats supported: full GID ("gid://shopify/Type/ID") or numeric string ("123456789").
func commonNew[T Identifiers](val any) (T, error) {
switch v := val.(type) {
case int64:
return T(int(v)), nil
case int:
return T(int(v)), nil
case string:
:= strings.Split(v, "/")
parts if len(parts) >= 5 {
// Handle full GID format: "gid://shopify/Type/ID"
:= typeFrom[T]()
typ if parts[3] != typ {
return T(0), fmt.Errorf("expected type %s got %s", typ, parts[3])
}
, err := strconv.ParseInt(parts[4], 10, 64)
cintif err != nil {
return T(0), fmt.Errorf("invalid ID in GID: %s", v)
}
return T(int(cint)), nil
} else {
// Handle numeric string format: "123456789"
, err := strconv.ParseInt(v, 10, 64)
cintif err != nil {
return T(0), fmt.Errorf("invalid GID format or numeric ID: %v", v)
}
return T(int(cint)), nil
}
default:
return T(0), fmt.Errorf("unsupported type for GID: %T", val)
}
}
// New creates a new GID from a value.
// It supports int64, int, and string formats.
// For strings, it expects the format "gid://shopify/Type/ID" or a
// numeric "28282823332".
// If the value is not recognized, it returns a zero value of the type.
// It ignores errors in validation, for validation use NewValidated.
func New[T Identifiers](val any) T {
, _ := commonNew[T](val)
nreturn n
}
// NewValidated creates a new GID from a value and validates it.
// It returns an error if the GID is not valid.
func NewValidated[T Identifiers](val any) (T, error) {
, err := commonNew[T](val)
gidif err != nil {
return gid, err
}
if !Identifier(gid).IsValid() {
return gid, fmt.Errorf("invalid %T value: %v", gid, val)
}
return gid, nil
}
// ...
-
commonNew
is a common method used by bothNew
andNewValidated
to create a value object -
typeFor
is a method to determine the expected type portion of the GID -
New
is a generic method for creating a value object of a type, without validation -
NewValidated
is a generic method for creating a value object of a type, with validation
Example implementation
An example implementation utilizing the Identifier
interface for a value object:
package gid
// CustomerID represents a GID for a Shopify Customer.
type CustomerID int
func (cid CustomerID) ID() int {
return int(cid)
}
func (cid CustomerID) String() string {
return fmt.Sprintf("gid://shopify/Customer/%d", cid)
}
func (cid CustomerID) Equal(ocid Identifier) bool {
if other, ok := ocid.(CustomerID); ok {
return cid == other
}
return false
}
func (cid CustomerID) MarshalJSON() ([]byte, error) {
return json.Marshal(cid.String())
}
func (cid *CustomerID) UnmarshalJSON(data []byte) error {
var gid string
if err := json.Unmarshal(data, &gid); err != nil {
return err
}
*cid = New[CustomerID](gid)
return nil
}
func (cid CustomerID) IsValid() bool {
return cid.ID() > 0
}
// CustomerIDs represents a slice of CustomerID.
type CustomerIDs []CustomerID
func (cids CustomerIDs) ToIDs() []int {
return ToIDs(cids)
}
func (cids CustomerIDs) ToStrings() []string {
return ToStrings(cids)
}
// NewCustomerID creates a new CustomerID from a value.
func NewCustomerID(val any) CustomerID {
return New[CustomerID](val)
}
// NewCustomerIDValidated creates a new CustomerID from a value and validates it.
func NewCustomerIDValidated(val any) (CustomerID, error) {
return NewValidated[CustomerID](val)
}
API
Init without validation
How to create and access information of the object.
These methods do not return an error for bad objects, it will return an object with a zero value instead. If you need validation, refer to validation section below this.
:= gid.NewCustomerID("gid://shopify/Customer/12345") // accepts GID or ID
cid .ID() // 12345
cid.String() // gid://shopify/Customer/12345
cid.IsValid() // true
cid
:= gid.NewOrderID(478848)
oid .ID() // 478848
oid.String() // gid://shopify/Order/478848
oid.IsValid() // true
oid
// Alternatively, by generic method.
:= gid.New[gid.ProductID](123) // gid.New[gid.ProductID]("gid://shopify/Product/123")
pid
// No error reported, but zero-value object returned.
:= gid.NewVariantID("gid://shopify/Whoops/1234")
vid .IsValid() // false vid
Init with validation
All New{X}ID
methods support validation by appending Validated
.
These methods will return an error and the object will be zero value.
, err := gid.NewCustomerIDValidated("whoops")
cid// err = "invalid CustomerID: whoops"
.IsValid() // false cid
Comparisons
How to compare two objects.
:= gid.NewCustomerID("gid://shopify/Customer/12345")
cid := gid.NewCustomerID("gid://shopify/Customer/123456")
cid2 := gid.NewCustomerID("gid://shopify/Customer/123456")
oid .Printf("Same? %v", cid.Equal(cid2)) // Same? false
fmt.Printf("Same? %v", cid.Equal(cid)) // Same? true
fmt.Printf("Same? %v", oid.Equal(cid)) // Same? false fmt
Slice supports
All built-in objects also have slices such as ProductIDs
, OrderIDs
, etc. to
do things like convert all objects to their IDs or all objects to their GID.
:= gid.ProductIDs{
pids .ProductID(1),
gid.ProductID(2),
gid.NewProductID("gid://shopify/Product/5"),
gid}
.ToIDs() // [1, 2, 5]
pids.ToStrings() // [gid://shopify/Product/1, gid://shopify/Product/2, gid://shopify/Product/5]
pids
// Generic versions.
.ToIDs([]gid.CustomerID{
gid.CustomerID(1),
gid.NewCustomerID("gid://shopify/Customer/5"),
gid}) // [1, 5]
.ToStrings([]gid.CustomerID{
gid.CustomerID(1),
gid.NewCustomerID("gid://shopify/Customer/5"),
gid}) // [gid://shopify/Customer/1, gid://shopify/Customer/5]
Marshalling/Unmarshalling
Automatic support for marshalling to JSON and from a struct into the value object type.
type something struct {
.OrderID `json:"order_id"`
OrderID gid// ...
}
- If Unmarshalled,
order_id
will be cased to aOrderID
value object. - If Marshalled,
order_id
will be turned intogid://shopify/Order/{id}
.
You can pass around the value objects and utilize them too:
func SomeFunc(open bool, variantID gid.VariantID) {
// ...
}
:= SomeFunc(true, gid.VariantID(129292)) sm
You can view the package snippet here on Github.