A type-safe nullable value library for Go using generics, designed for seamless JSON marshaling and database operations.
- Type-safe nullable values for any supported type using Go generics
- Database-friendly with built-in
sql.Scanneranddriver.Valuerimplementations - JSON marshaling that uses standard
nullinstead of{Valid: true, Value: ...} - PostgreSQL JSON/JSONB support for storing complex types
- UUID support with
github.com/google/uuid - Zero external dependencies (except
google/uuid) - Fully tested with comprehensive unit and integration tests
go get github.com/ovya/nullableimport "github.com/ovya/nullable"
// Create nullable values
name := nullable.FromValue("John Doe")
age := nullable.FromValue(30)
email := nullable.Null[string]() // Explicitly null
// Check if null
if name.IsNull() {
// Handle null case
}
// Get value
if !age.IsNull() {
fmt.Println(*age.GetValue()) // 30
}The library supports the following types through the Of[T] generic wrapper:
- Integers:
int,int16,int32,int64 - Floating point:
float64 - Boolean:
bool - String:
string - UUID:
uuid.UUID(fromgithub.com/google/uuid) - JSON:
nullable.JSON(alias forany) - for complex types stored as JSON in database
type User struct {
Name sql.NullString `json:"name"`
Age sql.NullInt64 `json:"age"`
}
// JSON output:
// {"name":{"String":"John","Valid":true},"age":{"Int64":30,"Valid":true}}type User struct {
Name nullable.Of[string] `json:"name"`
Age nullable.Of[int] `json:"age"`
}
// JSON output:
// {"name":"John","age":30}
// or with null values:
// {"name":null,"age":null}package main
import (
"encoding/json"
"fmt"
"github.com/ovya/nullable"
)
type User struct {
ID nullable.Of[int] `json:"id"`
Name nullable.Of[string] `json:"name"`
Email nullable.Of[string] `json:"email"`
Age nullable.Of[int] `json:"age"`
IsActive nullable.Of[bool] `json:"isActive"`
}
func main() {
// Create user with some null fields
user := User{
ID: nullable.FromValue(1),
Name: nullable.FromValue("John Doe"),
Email: nullable.Null[string](), // Null email
Age: nullable.FromValue(30),
IsActive: nullable.FromValue(true),
}
// Marshal to JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
// Output: {"id":1,"name":"John Doe","email":null,"age":30,"isActive":true}
// Unmarshal from JSON
jsonStr := `{"id":2,"name":"Jane Doe","email":"jane@example.com","age":null,"isActive":false}`
var user2 User
json.Unmarshal([]byte(jsonStr), &user2)
fmt.Println(*user2.Name.GetValue()) // "Jane Doe"
fmt.Println(user2.Age.IsNull()) // true
}import (
"database/sql"
"time"
"github.com/ovya/nullable"
_ "github.com/jackc/pgx/v5/stdlib"
)
type Article struct {
ID int64 `db:"id"`
Title nullable.Of[string] `db:"title"`
Content nullable.Of[string] `db:"content"`
PublishedAt nullable.Of[time.Time] `db:"published_at"`
AuthorID nullable.Of[int64] `db:"author_id"`
}
func insertArticle(db *sql.DB) error {
article := Article{
Title: nullable.FromValue("My Article"),
Content: nullable.FromValue("Article content here..."),
PublishedAt: nullable.FromValue(time.Now()),
AuthorID: nullable.Null[int64](), // Anonymous article
}
query := `
INSERT INTO articles (title, content, published_at, author_id)
VALUES ($1, $2, $3, $4)
RETURNING id
`
return db.QueryRow(
query,
article.Title,
article.Content,
article.PublishedAt,
article.AuthorID,
).Scan(&article.ID)
}func getArticle(db *sql.DB, id int64) (*Article, error) {
var article Article
query := `
SELECT id, title, content, published_at, author_id
FROM articles
WHERE id = $1
`
err := db.QueryRow(query, id).Scan(
&article.ID,
&article.Title,
&article.Content,
&article.PublishedAt,
&article.AuthorID,
)
if err != nil {
return nil, err
}
return &article, nil
}Store complex Go types as JSON in PostgreSQL:
type Metadata struct {
Tags []string `json:"tags"`
Properties map[string]string `json:"properties"`
Version int `json:"version"`
}
type Document struct {
ID int64 `db:"id"`
Title nullable.Of[string] `db:"title"`
Metadata nullable.Of[nullable.JSON] `db:"metadata"` // Stored as JSONB
}
func insertDocument(db *sql.DB) error {
meta := Metadata{
Tags: []string{"golang", "database"},
Properties: map[string]string{"type": "article", "lang": "en"},
Version: 1,
}
doc := Document{
Title: nullable.FromValue("Go Nullable Guide"),
Metadata: nullable.FromValue[nullable.JSON](meta),
}
query := `INSERT INTO documents (title, metadata) VALUES ($1, $2) RETURNING id`
return db.QueryRow(query, doc.Title, doc.Metadata).Scan(&doc.ID)
}type Address struct {
Street nullable.Of[string] `json:"street"`
City nullable.Of[string] `json:"city"`
ZipCode nullable.Of[string] `json:"zipCode"`
}
type Profile struct {
Bio nullable.Of[string] `json:"bio"`
Website nullable.Of[string] `json:"website"`
Address nullable.Of[nullable.JSON] `json:"address"`
}
type User struct {
Username nullable.Of[string] `json:"username"`
Email nullable.Of[string] `json:"email"`
Profile nullable.Of[nullable.JSON] `json:"profile"`
}
func main() {
user := User{
Username: nullable.FromValue("johndoe"),
Email: nullable.FromValue("john@example.com"),
Profile: nullable.FromValue[nullable.JSON](Profile{
Bio: nullable.FromValue("Software Developer"),
Website: nullable.FromValue("https://johndoe.com"),
Address: nullable.FromValue[nullable.JSON](Address{
Street: nullable.FromValue("123 Main St"),
City: nullable.FromValue("New York"),
ZipCode: nullable.FromValue("10001"),
}),
}),
}
data, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(data))
}For custom primitive types that should be stored as their underlying type (not JSON):
import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
)
type PhoneNumber string
// Value implements driver.Valuer to store as string in database
func (pn PhoneNumber) Value() (driver.Value, error) {
return string(pn), nil
}
// Scan implements sql.Scanner to read from database
func (pn *PhoneNumber) Scan(v any) error {
switch val := v.(type) {
case int, int64, uint64:
*pn = PhoneNumber(strconv.Itoa(val.(int)))
case string:
*pn = PhoneNumber(val)
default:
return errors.New(fmt.Sprintf("cannot scan phone number from type %T", val))
}
return nil
}
// Now PhoneNumber will be stored as string, not JSON
type Contact struct {
Email nullable.Of[string] `db:"email"`
Phone nullable.Of[PhoneNumber] `db:"phone"` // Stored as string, not JSON
}// From a value
name := nullable.FromValue("John")
// Explicitly null
email := nullable.Null[string]()
// From a pointer (nil pointer becomes null)
var ptr *string = nil
value := nullable.Of[string]{}
value.SetValueP(ptr) // Sets to null// Check if null
if value.IsNull() {
// Handle null
}
// Get value (returns *T)
if !value.IsNull() {
v := value.GetValue()
fmt.Println(*v)
}var value nullable.Of[string]
// Set a value
value.SetValue("hello")
// Set from pointer
str := "world"
value.SetValueP(&str)
// Set to null
value.SetNull()// Marshal
data, err := json.Marshal(value)
// Unmarshal
var value nullable.Of[string]
err := json.Unmarshal([]byte(`"hello"`), &value)
// Unmarshal null
err := json.Unmarshal([]byte(`null`), &value)
// value.IsNull() == trueRun all tests including PostgreSQL integration tests:
cd tests
go test -v ./...Or from the root:
make testRequirements:
- Docker must be running (testcontainers uses Docker to spin up PostgreSQL)
- No manual database setup needed - testcontainers handles everything
First run: Tests will download the PostgreSQL 18 image (~80MB), subsequent runs use cached image.
Run only unit tests (no database required):
cd tests
go test -run 'TestMarshal|TestUnmarshal|TestNullableEdgeCases' -v| Feature | nullable |
database/sql.Null* |
gopkg.in/guregu/null.v4 |
|---|---|---|---|
| Type-safe generics | ✅ | ❌ (separate type per kind) | ❌ (separate type per kind) |
| Clean JSON output | ✅ null |
❌ {"Valid":false} |
✅ null |
| PostgreSQL JSON/JSONB | ✅ | ❌ | |
| UUID support | ✅ | ❌ | ❌ |
| Custom types | ✅ via Scanner/Valuer | ✅ via Scanner/Valuer | ✅ via Scanner/Valuer |
| Zero dependencies* | ✅ | ✅ | ❌ |
*Except google/uuid for UUID support
The opt package is another modern approach to nullable values in Go, but with a fundamentally different philosophy.
nullable (2-state model):
type User struct {
Name nullable.Of[string] // Can be: null OR "John"
}
// Zero value is null
// No distinction between "field not provided" and "field set to null"opt (3-state model):
import "github.com/aarondl/opt/omitnull"
type User struct {
Name omitnull.Val[string] // Can be: unset OR null OR "John"
}
// Zero value is unset (omitted)
// Distinguishes: not provided vs explicitly null vs actual valueThe distinction between "unset" and "null" is crucial for partial API updates:
// Request 1: Update name, clear age
{"name": "John", "age": null}
// Request 2: Update name only, don't touch age
{"name": "John"}
// Request 3: Clear both fields
{"name": null, "age": null}With nullable: Cannot distinguish between Request 1 and Request 2 (both result in IsNull() == true)
With opt: Can distinguish all three scenarios:
if req.Name.IsUnset() {
// Don't update (Request 2)
} else if req.Name.IsNull() {
// Set to NULL (Request 3)
} else {
// Update with value (Request 1)
}| Feature | nullable |
opt |
|---|---|---|
| State Model | 2-state (null/value) | 3-state (unset/null/value) |
| Zero Value | null |
unset |
| Clean JSON | ✅ | ✅ |
| Database Operations | ✅ | ✅ |
| Partial Updates | ❌ | ✅ |
| Distinguish unset vs null | ❌ | ✅ |
| Type Constraints | ✅ (safer) | ❌ (any type) |
| PostgreSQL JSON/JSONB | ✅ Optimized | ✅ Generic |
| UUID Support | ✅ Built-in | ✅ Any type |
| Functional Operations | ❌ | ✅ Map(), etc. |
| Package Structure | Single type | 3 sub-packages |
| Maturity | Stable | Pre-1.0 |
Creating Values:
// nullable
name := nullable.FromValue("John")
email := nullable.Null[string]()
// opt
import "github.com/aarondl/opt/omitnull"
name := omitnull.From("John")
email := omitnull.FromNull[string]()
unset := omitnull.Val[string]{} // unset stateChecking State:
// nullable - 2 checks
if value.IsNull() {
// null or zero value
}
// opt - 3 distinct checks
if value.IsUnset() {
// field omitted
} else if value.IsNull() {
// explicitly null
} else if value.IsValue() {
// has value
}Getting Values:
// nullable
if !value.IsNull() {
v := value.GetValue() // *T
fmt.Println(*v)
}
// opt - more options
v, ok := value.Get() // (T, bool)
v := value.GetOr("default") // with fallback
v := value.MustGet() // panics if not set
ptr := value.Ptr() // *T or nil// With opt - perfect for partial updates
type UpdateUserRequest struct {
Name omitnull.Val[string] `json:"name"`
Email omitnull.Val[string] `json:"email"`
Age omitnull.Val[int] `json:"age"`
}
func UpdateUser(req UpdateUserRequest) {
query := "UPDATE users SET "
var updates []string
var args []any
if req.Name.IsValue() {
updates = append(updates, "name = ?")
args = append(args, req.Name.MustGet())
} else if req.Name.IsNull() {
updates = append(updates, "name = NULL")
}
// else: IsUnset() - don't touch this field
// Same pattern for Email and Age...
}
// Handles all these requests correctly:
// {} → no updates
// {"name": "John"} → update name only
// {"name": "John", "age": null} → update name, clear age
// {"name": null, "age": null} → clear bothChoose nullable when:
- Building typical CRUD applications
- You need clean JSON marshaling for database types
- Simpler 2-state model (null/value) fits your needs
- You want type safety with constraints
- You prefer a simpler API
Choose opt when:
- Building REST APIs with PATCH endpoints
- You need to distinguish "omitted" from "null"
- Implementing partial update semantics
- Working with GraphQL (handles optional/nullable distinction)
- You need support for any type (not just specific types)
- You want functional operations like
Map()
Both packages solve the "clean JSON marshaling" problem well. The key difference is whether you need 2 states (null/value) or 3 states (unset/null/value).
This project was inspired by gonull, which had issues with PostgreSQL types like enum, timestamp, and json/jsonb. This library addresses those limitations while providing a cleaner API.
Contributions are welcome! Please feel free to submit a Pull Request.
See LICENSE file for details.