Type-safe optional values for Go - marshal/unmarshal to/from JSON, YAML, SQL and more.
go get github.com/kazhuravlev/optionalRequires Go 1.23 or newer.
package main
import (
"encoding/json"
"fmt"
"github.com/kazhuravlev/optional"
)
type User struct {
Name string `json:"name"`
AvatarURL optional.Val[string] `json:"avatar_url,omitzero"` // omitzero needs Go 1.24+
}
func main() {
// JSON with optional field
data := []byte(`{"name": "Alice", "avatar_url": "https://example.com/avatar.jpg"}`)
var user User
json.Unmarshal(data, &user)
// Check if value exists
if avatarURL, ok := user.AvatarURL.Get(); ok {
fmt.Printf("Avatar URL: %s\n", avatarURL)
}
// Or use a default
url := user.AvatarURL.ValDefault("https://example.com/default.jpg")
fmt.Printf("URL with default: %s\n", url)
}- Type-safe: Generic implementation prevents runtime type errors
- Zero value friendly: Missing values are not mistaken for zero values
- Multiple formats: Works with JSON, YAML, SQL, and custom marshalers
- Simple API: Intuitive methods like
Get(),Set(),HasVal(), andReset()
var opt optional.Val[string] // the zero value is an empty container
// Set a value
opt.Set("hello")
// Check and get
if val, ok := opt.Get(); ok {
fmt.Println(val) // "hello"
}
// Get with default
val := opt.ValDefault("default") // "hello"
// Reset to empty
opt.Reset()
// Convert to pointer (nil if empty)
ptr := opt.AsPointer() // *string
// Empty? (also used by json omitzero and yaml omitempty)
opt.IsZero() // true- An empty container is encoded as
null;nulland a missing field decode as empty. - Decoding
nullover a container that already has a value clears it. - Use
json:",omitzero"(Go 1.24+) to drop empty containers from the output.omitemptyhas no effect on struct types, soValis always emitted with it. - A present
nilslice ornilpointer is also encoded asnulland therefore decodes as empty: "present but null" cannot be represented.
- An empty container is encoded as
null;yaml:",omitempty"drops it. null,~and an empty value decode as empty when the destination is a zero struct. yaml.v3 never hands null nodes to custom unmarshalers, so decodingnullover a container that already holds a value leaves it unchanged. Use*optional.Val[T]when "null clears the value" matters.- yaml.v3 drops
nullelements from slices of non-pointer element types ([]Val[T]behaves like[]stringhere);[]*Val[T]keeps them asnil.
NULLscans as empty, even whenTimplementssql.Scanneritself.- A
Tthat implementssql.Scannerscans in place, so state prepared on the value beforehand stays available to it. Other values are converted like database/sql does for plain destinations:int64into any integer type,[]byteintostring, and so on.[]byte(andsql.RawBytes) is copied, so the container never aliases a buffer owned by the driver. Value()asks aTthat implementsdriver.Valuerfor its value, then normalises the result to thedriver.Valuesubset where possible (intandtime.Durationbecomeint64, named string types becomestring). Values the default converter rejects, such as slices, maps or structs, are handed to the driver unchanged: pgx encodes them itself, strict drivers report their own error.