#[1]gnikyt feed [2]gnikyt / Code ramblings [3]Ty King [4]About[5]Github[6]LinkedIn[7]CV[8]RSS Shopify Value Objects for Go / /* Jul 15, 2025 — 8.7KB */ [9]Logo of golang 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 { CustomerID | ProductID | VariantID | OrderID | InventoryItem } // Identifier represents a GID. type Identifier interface { ID() int String() string Equal(ogid Identifier) bool IsValid() bool } // ... 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 { ids := make([]int, len(gids)) for i, gid := range gids { ids[i] = Identifier(gid).ID() } return ids } // ToStrings converts GIDs to their string representation. func ToStrings[T Identifiers](gids []T) []string { strs := make([]string, len(gids)) for i, gid := range gids { strs[i] = Identifier(gid).String() } 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 strin g ("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: parts := strings.Split(v, "/") if len(parts) >= 5 { // Handle full GID format: "gid://shopify/Type/ID" typ := typeFrom[T]() if parts[3] != typ { return T(0), fmt.Errorf("expected type %s got %s", typ, parts[3] ) } cint, err := strconv.ParseInt(parts[4], 10, 64) if err != nil { return T(0), fmt.Errorf("invalid ID in GID: %s", v) } return T(int(cint)), nil } else { // Handle numeric string format: "123456789" cint, err := strconv.ParseInt(v, 10, 64) if 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 { n, _ := commonNew[T](val) return 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) { gid, err := commonNew[T](val) if 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 both New and NewValidated 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. cid := gid.NewCustomerID("gid://shopify/Customer/12345") // accepts GID or ID cid.ID() // 12345 cid.String() // gid://shopify/Customer/12345 cid.IsValid() // true oid := gid.NewOrderID(478848) oid.ID() // 478848 oid.String() // gid://shopify/Order/478848 oid.IsValid() // true // Alternatively, by generic method. pid := gid.New[gid.ProductID](123) // gid.New[gid.ProductID]("gid://shopify/Prod uct/123") // No error reported, but zero-value object returned. vid := gid.NewVariantID("gid://shopify/Whoops/1234") vid.IsValid() // false 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. cid, err := gid.NewCustomerIDValidated("whoops") // err = "invalid CustomerID: whoops" cid.IsValid() // false Comparisons How to compare two objects. cid := gid.NewCustomerID("gid://shopify/Customer/12345") cid2 := gid.NewCustomerID("gid://shopify/Customer/123456") oid := gid.NewCustomerID("gid://shopify/Customer/123456") fmt.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 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. pids := gid.ProductIDs{ gid.ProductID(1), gid.ProductID(2), gid.NewProductID("gid://shopify/Product/5"), } pids.ToIDs() // [1, 2, 5] pids.ToStrings() // [gid://shopify/Product/1, gid://shopify/Product/2, gid://sho pify/Product/5] // Generic versions. gid.ToIDs([]gid.CustomerID{ gid.CustomerID(1), gid.NewCustomerID("gid://shopify/Customer/5"), }) // [1, 5] gid.ToStrings([]gid.CustomerID{ gid.CustomerID(1), gid.NewCustomerID("gid://shopify/Customer/5"), }) // [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 gid.OrderID `json:"order_id"` // ... } * If Unmarshalled, order_id will be cased to a OrderID value object. * If Marshalled, order_id will be turned into gid://shopify/Order/{id}. You can pass around the value objects and utilize them too: func SomeFunc(open bool, variantID gid.VariantID) { // ... } sm := SomeFunc(true, gid.VariantID(129292)) You can view the package snippet [10]here on Github. Anchors * [1] [11]github.com/gnikyt/shopify-go-value-objects ↗ Appendix Copyright under [12]CC-4.0. Available in the following alternative formats: [13]MD / [14]TXT / [15]PDF * * * * * * * * References 1. /rss.xml 2. file:/// 3. file:///about 4. file:///about 5. https://github.com/gnikyt 6. https://linkedin.com/in/gnikyt 7. file:///assets/files/cv.pdf 8. file:///rss.xml 9. file:///category/golang 10. https://github.com/gnikyt/shopify-go-value-objects 11. https://github.com/gnikyt/shopify-go-value-objects 12. https://creativecommons.org/licenses/by/4.0/ 13. file:///shopify-value-objects-go/index.md 14. file:///shopify-value-objects-go/index.txt 15. file:///tmp/lynxXXXXjG3XMu/L718882-3880TMP.html