Arbitrary-precision fixed-point decimal numbers for Go, avoiding floating-point precision issues with support for arithmetic, rounding, serialization, and database integration.
Complete guide to JSON serialization and deserialization with Decimal values.
import (
"encoding/json"
"github.com/shopspring/decimal"
)
type Product struct {
Name string `json:"name"`
Price decimal.Decimal `json:"price"`
}
// Marshal (default: quoted string)
p := Product{
Name: "Widget",
Price: decimal.NewFromString("19.99"),
}
data, _ := json.Marshal(p)
// {"name":"Widget","price":"19.99"}
// Unmarshal (accepts both quoted and unquoted)
var p2 Product
json.Unmarshal([]byte(`{"name":"Widget","price":"19.99"}`), &p2)
json.Unmarshal([]byte(`{"name":"Widget","price":19.99}`), &p2) // Also worksBy default, Decimal marshals to JSON as a quoted string to preserve precision.
type Price struct {
Amount decimal.Decimal `json:"amount"`
}
p := Price{Amount: decimal.NewFromString("19.99")}
data, _ := json.Marshal(p)
// Output: {"amount":"19.99"}
// ^^^^^^^^^^^^^^^^^ Quoted stringWhy quoted strings?
var p Price
// Accepts quoted string
json.Unmarshal([]byte(`{"amount":"19.99"}`), &p) // ✓
// Also accepts unquoted number
json.Unmarshal([]byte(`{"amount":19.99}`), &p) // ✓
// Both produce same result
fmt.Println(p.Amount.String()) // "19.99"// Enable globally (affects all Decimal values)
decimal.MarshalJSONWithoutQuotes = true
p := Price{Amount: decimal.NewFromString("19.99")}
data, _ := json.Marshal(p)
// Output: {"amount":19.99}
// ^^^^^^^^^^^^^^ Unquoted number⚠ WARNING: Precision Loss Risk
99999999999999999 becomes 100000000000000000 in JSMarshalJSONWithoutQuotes = falseUse unquoted numbers ONLY if:
Example use case:
// API for simple percentages (low precision need)
type Config struct {
Rate decimal.Decimal `json:"rate"` // 0.08
}
decimal.MarshalJSONWithoutQuotes = truetype ProductResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Price decimal.Decimal `json:"price"`
Discount decimal.Decimal `json:"discount,omitempty"`
TaxRate decimal.Decimal `json:"tax_rate"`
}
func productHandler(w http.ResponseWriter, r *http.Request) {
product := ProductResponse{
ID: 1,
Name: "Widget",
Price: decimal.NewFromString("99.99"),
Discount: decimal.NewFromString("10.00"),
TaxRate: decimal.NewFromFloat(0.08),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(product)
}
// Response:
// {
// "id": 1,
// "name": "Widget",
// "price": "99.99",
// "discount": "10.00",
// "tax_rate": "0.08"
// }type CreateProductRequest struct {
Name string `json:"name"`
Price decimal.Decimal `json:"price"`
Discount decimal.Decimal `json:"discount"`
}
func createProductHandler(w http.ResponseWriter, r *http.Request) {
var req CreateProductRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Validate
if req.Price.LessThanOrEqual(decimal.Zero) {
http.Error(w, "Price must be positive", http.StatusBadRequest)
return
}
// Use req.Price and req.Discount
// ...
}
// Accepts:
// {"name":"Widget","price":"99.99","discount":"10.00"}
// OR
// {"name":"Widget","price":99.99,"discount":10.00}type Order struct {
ID int `json:"id"`
Items []OrderItem `json:"items"`
Subtotal decimal.Decimal `json:"subtotal"`
Tax decimal.Decimal `json:"tax"`
Total decimal.Decimal `json:"total"`
}
type OrderItem struct {
ProductID int `json:"product_id"`
Name string `json:"name"`
Quantity int `json:"quantity"`
Price decimal.Decimal `json:"price"`
ItemTotal decimal.Decimal `json:"item_total"`
}
func calculateOrder(items []OrderItem) Order {
subtotal := decimal.Zero
for i := range items {
items[i].ItemTotal = items[i].Price.Mul(
decimal.NewFromInt(int64(items[i].Quantity)),
)
subtotal = subtotal.Add(items[i].ItemTotal)
}
taxRate := decimal.NewFromFloat(0.08)
tax := subtotal.Mul(taxRate).Round(2)
total := subtotal.Add(tax)
return Order{
Items: items,
Subtotal: subtotal,
Tax: tax,
Total: total,
}
}type Product struct {
Name string `json:"name"`
Price decimal.Decimal `json:"price"`
Discount decimal.NullDecimal `json:"discount"` // May be null
}
// With discount
p1 := Product{
Name: "Widget",
Price: decimal.NewFromString("99.99"),
Discount: decimal.NewNullDecimal(decimal.NewFromString("10.00")),
}
json.Marshal(p1)
// {"name":"Widget","price":"99.99","discount":"10.00"}
// Without discount (null)
p2 := Product{
Name: "Widget",
Price: decimal.NewFromString("99.99"),
Discount: decimal.NullDecimal{Valid: false},
}
json.Marshal(p2)
// {"name":"Widget","price":"99.99","discount":null}var p Product
// With value
json.Unmarshal([]byte(`{"name":"Widget","price":"99.99","discount":"10.00"}`), &p)
// p.Discount.Valid = true, p.Discount.Decimal = 10.00
// With null
json.Unmarshal([]byte(`{"name":"Widget","price":"99.99","discount":null}`), &p)
// p.Discount.Valid = false
// With missing field (treated as null)
json.Unmarshal([]byte(`{"name":"Widget","price":"99.99"}`), &p)
// p.Discount.Valid = falsetype UpdateProductRequest struct {
Name *string `json:"name,omitempty"`
Price *decimal.Decimal `json:"price,omitempty"`
Discount *decimal.NullDecimal `json:"discount,omitempty"`
}
func updateProduct(productID int, updates UpdateProductRequest) error {
if updates.Name != nil {
// Update name
}
if updates.Price != nil {
// Update price
}
if updates.Discount != nil {
if updates.Discount.Valid {
// Set discount
} else {
// Clear discount (set to NULL)
}
}
return nil
}type Product struct {
Price decimal.Decimal `json:"price"`
Discount decimal.Decimal `json:"discount,omitempty"` // Omit if zero
}
p := Product{Price: decimal.NewFromString("99.99")}
json.Marshal(p)
// {"price":"99.99"} (discount omitted because it's zero)Note: omitempty omits zero values, not NULL values. For true NULL, use NullDecimal.
type Product struct {
UnitPrice decimal.Decimal `json:"unit_price"`
ListPrice decimal.Decimal `json:"list_price"`
SalePrice decimal.Decimal `json:"sale_price,omitempty"`
}type Product struct {
Price decimal.Decimal `json:"price"`
InternalCost decimal.Decimal `json:"-"` // Never marshaled
}var p Product
data := []byte(`{"name":"Widget","price":"invalid"}`)
err := json.Unmarshal(data, &p)
if err != nil {
// Handle invalid decimal in JSON
fmt.Println("Invalid JSON:", err)
}type CreateProductRequest struct {
Price decimal.Decimal `json:"price"`
}
func handleCreate(data []byte) error {
var req CreateProductRequest
if err := json.Unmarshal(data, &req); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
// Validate
if req.Price.LessThanOrEqual(decimal.Zero) {
return errors.New("price must be positive")
}
if req.Price.GreaterThan(decimal.NewFromInt(1000000)) {
return errors.New("price exceeds maximum")
}
return nil
}type PriceBreakdown struct {
Subtotal decimal.Decimal `json:"subtotal"`
Tax decimal.Decimal `json:"tax"`
Shipping decimal.Decimal `json:"shipping"`
Discount decimal.Decimal `json:"discount"`
Total decimal.Decimal `json:"total"`
}
func calculateBreakdown(subtotal decimal.Decimal) PriceBreakdown {
taxRate := decimal.NewFromFloat(0.08)
tax := subtotal.Mul(taxRate).Round(2)
shipping := decimal.NewFromString("5.99")
discountRate := decimal.NewFromFloat(0.10)
discount := subtotal.Mul(discountRate).Round(2)
total := subtotal.Add(tax).Add(shipping).Sub(discount)
return PriceBreakdown{
Subtotal: subtotal,
Tax: tax,
Shipping: shipping,
Discount: discount,
Total: total,
}
}
// Marshal for API response
breakdown := calculateBreakdown(decimal.NewFromString("100.00"))
json.Marshal(breakdown)type AppConfig struct {
TaxRates map[string]decimal.Decimal `json:"tax_rates"`
Limits struct {
MinOrder decimal.Decimal `json:"min_order"`
MaxOrder decimal.Decimal `json:"max_order"`
} `json:"limits"`
}
// Load from JSON file
func loadConfig(filename string) (*AppConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var config AppConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
// config.json:
// {
// "tax_rates": {
// "CA": "0.0825",
// "NY": "0.08875"
// },
// "limits": {
// "min_order": "10.00",
// "max_order": "10000.00"
// }
// }type GraphQLResponse struct {
Data struct {
Product struct {
ID int `json:"id"`
Price decimal.Decimal `json:"price"`
} `json:"product"`
} `json:"data"`
}
func queryProduct(id int) (*GraphQLResponse, error) {
query := `{"query": "{ product(id: %d) { id price } }"}`
// ... make GraphQL request
// ... unmarshal response
var response GraphQLResponse
json.Unmarshal(data, &response)
return &response, nil
}// Streaming for large datasets
type Product struct {
Price decimal.Decimal `json:"price"`
}
// Stream encoding
func encodeProducts(w io.Writer, products []Product) error {
encoder := json.NewEncoder(w)
for _, p := range products {
if err := encoder.Encode(p); err != nil {
return err
}
}
return nil
}
// Stream decoding
func decodeProducts(r io.Reader) ([]Product, error) {
var products []Product
decoder := json.NewDecoder(r)
for decoder.More() {
var p Product
if err := decoder.Decode(&p); err != nil {
return nil, err
}
products = append(products, p)
}
return products, nil
}// Reuse encoder for multiple writes
encoder := json.NewEncoder(w)
for _, product := range products {
if err := encoder.Encode(product); err != nil {
return err
}
}func TestProductJSON(t *testing.T) {
original := Product{
Name: "Widget",
Price: decimal.NewFromString("19.99"),
}
// Marshal
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("marshal error: %v", err)
}
// Unmarshal
var decoded Product
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
// Compare
if !decoded.Price.Equal(original.Price) {
t.Errorf("price mismatch: got %s, want %s",
decoded.Price, original.Price)
}
}func TestUnmarshalProduct(t *testing.T) {
tests := []struct {
name string
json string
want decimal.Decimal
wantErr bool
}{
{
name: "quoted string",
json: `{"price":"19.99"}`,
want: decimal.NewFromString("19.99"),
},
{
name: "unquoted number",
json: `{"price":19.99}`,
want: decimal.NewFromFloat(19.99),
},
{
name: "invalid",
json: `{"price":"invalid"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var p Product
err := json.Unmarshal([]byte(tt.json), &p)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !p.Price.Equal(tt.want) {
t.Errorf("got %s, want %s", p.Price, tt.want)
}
})
}
}{"price": "19.99"}Pros: Precision preserved, safe Cons: Larger payload, needs parsing
{"price": 19.99}Pros: Smaller payload, native JSON Cons: Precision loss risk, JavaScript issues
For binary protocols, use MarshalBinary / UnmarshalBinary:
data, _ := price.MarshalBinary() // []byte
var decoded decimal.Decimal
decoded.UnmarshalBinary(data)Install with Tessl CLI
npx tessl i tessl/golang-github-com-shopspring--decimal