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 valuesname:=nullable.FromValue("John Doe")
age:=nullable.FromValue(30)
email:=nullable.Null[string]() // Explicitly null// Check if nullifname.IsNull() {
// Handle null case
}
// Get valueif!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
typeUserstruct {
Name sql.NullString`json:"name"`Age sql.NullInt64`json:"age"`
}
// JSON output:// {"name":{"String":"John","Valid":true},"age":{"Int64":30,"Valid":true}}typeUserstruct {
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"
)
typeUserstruct {
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"`
}
funcmain() {
// Create user with some null fieldsuser:=User{
ID: nullable.FromValue(1),
Name: nullable.FromValue("John Doe"),
Email: nullable.Null[string](), // Null emailAge: nullable.FromValue(30),
IsActive: nullable.FromValue(true),
}
// Marshal to JSONdata, _:=json.Marshal(user)
fmt.Println(string(data))
// Output: {"id":1,"name":"John Doe","email":null,"age":30,"isActive":true}// Unmarshal from JSONjsonStr:=`{"id":2,"name":"Jane Doe","email":"jane@example.com","age":null,"isActive":false}`varuser2Userjson.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"
)
typeArticlestruct {
IDint64`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"`
}
funcinsertArticle(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 `returndb.QueryRow(
query,
article.Title,
article.Content,
article.PublishedAt,
article.AuthorID,
).Scan(&article.ID)
}funcgetArticle(db*sql.DB, idint64) (*Article, error) {
vararticleArticlequery:=` 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,
)
iferr!=nil {
returnnil, err
}
return&article, nil
}Store complex Go types as JSON in PostgreSQL:
typeMetadatastruct {
Tags []string`json:"tags"`Propertiesmap[string]string`json:"properties"`Versionint`json:"version"`
}
typeDocumentstruct {
IDint64`db:"id"`Title nullable.Of[string] `db:"title"`Metadata nullable.Of[nullable.JSON] `db:"metadata"`// Stored as JSONB
}
funcinsertDocument(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`returndb.QueryRow(query, doc.Title, doc.Metadata).Scan(&doc.ID)
}typeAddressstruct {
Street nullable.Of[string] `json:"street"`City nullable.Of[string] `json:"city"`ZipCode nullable.Of[string] `json:"zipCode"`
}
typeProfilestruct {
Bio nullable.Of[string] `json:"bio"`Website nullable.Of[string] `json:"website"`Address nullable.Of[nullable.JSON] `json:"address"`
}
typeUserstruct {
Username nullable.Of[string] `json:"username"`Email nullable.Of[string] `json:"email"`Profile nullable.Of[nullable.JSON] `json:"profile"`
}
funcmain() {
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"
)
typePhoneNumberstring// Value implements driver.Valuer to store as string in databasefunc (pnPhoneNumber) Value() (driver.Value, error) {
returnstring(pn), nil
}
// Scan implements sql.Scanner to read from databasefunc (pn*PhoneNumber) Scan(vany) error {
switchval:=v.(type) {
caseint, int64, uint64:
*pn=PhoneNumber(strconv.Itoa(val.(int)))
casestring:
*pn=PhoneNumber(val)
default:
returnerrors.New(fmt.Sprintf("cannot scan phone number from type %T", val))
}
returnnil
}
// Now PhoneNumber will be stored as string, not JSONtypeContactstruct {
Email nullable.Of[string] `db:"email"`Phone nullable.Of[PhoneNumber] `db:"phone"`// Stored as string, not JSON
}// From a valuename:=nullable.FromValue("John")
// Explicitly nullemail:=nullable.Null[string]()
// From a pointer (nil pointer becomes null)varptr*string=nilvalue:= nullable.Of[string]{}
value.SetValueP(ptr) // Sets to null// Check if nullifvalue.IsNull() {
// Handle null
}
// Get value (returns *T)if!value.IsNull() {
v:=value.GetValue()
fmt.Println(*v)
}varvalue nullable.Of[string]
// Set a valuevalue.SetValue("hello")
// Set from pointerstr:="world"value.SetValueP(&str)
// Set to nullvalue.SetNull()// Marshaldata, err:=json.Marshal(value)
// Unmarshalvarvalue nullable.Of[string]
err:=json.Unmarshal([]byte(`"hello"`), &value)
// Unmarshal nullerr:=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):
typeUserstruct {
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"typeUserstruct {
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:
ifreq.Name.IsUnset() {
// Don't update (Request 2)
} elseifreq.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:
// nullablename:=nullable.FromValue("John")
email:=nullable.Null[string]()
// optimport"github.com/aarondl/opt/omitnull"name:=omitnull.From("John")
email:=omitnull.FromNull[string]()
unset:= omitnull.Val[string]{} // unset stateChecking State:
// nullable - 2 checksifvalue.IsNull() {
// null or zero value
}
// opt - 3 distinct checksifvalue.IsUnset() {
// field omitted
} elseifvalue.IsNull() {
// explicitly null
} elseifvalue.IsValue() {
// has value
}Getting Values:
// nullableif!value.IsNull() {
v:=value.GetValue() // *Tfmt.Println(*v)
}
// opt - more optionsv, ok:=value.Get() // (T, bool)v:=value.GetOr("default") // with fallbackv:=value.MustGet() // panics if not setptr:=value.Ptr() // *T or nil// With opt - perfect for partial updatestypeUpdateUserRequeststruct {
Name omitnull.Val[string] `json:"name"`Email omitnull.Val[string] `json:"email"`Age omitnull.Val[int] `json:"age"`
}
funcUpdateUser(reqUpdateUserRequest) {
query:="UPDATE users SET "varupdates []stringvarargs []anyifreq.Name.IsValue() {
updates=append(updates, "name = ?")
args=append(args, req.Name.MustGet())
} elseifreq.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.