Skip to content

Repository files navigation

Go Nullable

golangci-lintmod-verifygosecstaticchecktest

This library was developed by pivaldi and is now maintained and extended HERE.

A type-safe nullable value library for Go using generics, designed for seamless JSON marshaling and database operations.

Features

  • Type-safe nullable values for any supported type using Go generics
  • Database-friendly with built-in sql.Scanner and driver.Valuer implementations
  • JSON marshaling that uses standard null instead 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

Installation

go get github.com/ovya/nullable

Quick Start

import"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
}

Supported Types

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 (from github.com/google/uuid)
  • JSON: nullable.JSON (alias for any) - for complex types stored as JSON in database

Why Use This Library?

Standard database/sql Approach

typeUserstruct {
Name sql.NullString`json:"name"`Age sql.NullInt64`json:"age"`
}
// JSON output:// {"name":{"String":"John","Valid":true},"age":{"Int64":30,"Valid":true}}

With nullable

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}

Usage Examples

Basic Usage

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
}

Database Operations

Insert

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)
}

Query

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
}

Working with JSON/JSONB (PostgreSQL)

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)
}

Nested Structures

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))
}

Custom Types with Scanner/Valuer

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
}

API Reference

Creating Nullable Values

// 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

Checking and Accessing Values

// Check if nullifvalue.IsNull() {
// Handle null
}
// Get value (returns *T)if!value.IsNull() {
v:=value.GetValue()
fmt.Println(*v)
}

Setting Values

varvalue nullable.Of[string]
// Set a valuevalue.SetValue("hello")
// Set from pointerstr:="world"value.SetValueP(&str)
// Set to nullvalue.SetNull()

JSON Operations

// Marshaldata, err:=json.Marshal(value)
// Unmarshalvarvalue nullable.Of[string]
err:=json.Unmarshal([]byte(`"hello"`), &value)
// Unmarshal nullerr:=json.Unmarshal([]byte(`null`), &value)
// value.IsNull() == true

Testing

Run all tests including PostgreSQL integration tests:

cd tests
go test -v ./...

Or from the root:

make test

Requirements:

  • 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

Comparison with Alternatives

Featurenullabledatabase/sql.Null*gopkg.in/guregu/null.v4
Type-safe generics❌ (separate type per kind)❌ (separate type per kind)
Clean JSON outputnull{"Valid":false}null
PostgreSQL JSON/JSONB⚠️ Limited
UUID support
Custom types✅ via Scanner/Valuer✅ via Scanner/Valuer✅ via Scanner/Valuer
Zero dependencies*

*Except google/uuid for UUID support

Detailed Comparison with aarondl/opt

The opt package is another modern approach to nullable values in Go, but with a fundamentally different philosophy.

Core Difference: 2-State vs 3-State Model

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 value

When the 3-State Model Matters

The 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 Comparison

Featurenullableopt
State Model2-state (null/value)3-state (unset/null/value)
Zero Valuenullunset
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 OperationsMap(), etc.
Package StructureSingle type3 sub-packages
MaturityStablePre-1.0

API Comparison

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 state

Checking 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

Real-World Example: PATCH Endpoint

// 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 both

When to Choose Each

Choose 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).

Similar Projects

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.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

See LICENSE file for details.

About

Go database null values for any data type thanks to the Golang generic features.

Topics

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages