null

package module
v0.0.0-...-12622cd Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 7, 2026 License: MIT Imports: 6 Imported by: 0

README

null

Go Reference Go Report Card CI codecov

Generic nullable type for Go that distinguishes between unset, null, and valid values.

Features

  • Three-State Logic — Distinguish between "not provided", "explicitly null", and "has value"
  • Generic — Single Value[T] type works with any type
  • JSON Support — Full marshal/unmarshal with three-state preservation
  • SQL Support — Implements Scanner and Valuer for all common types
  • DynamoDB Support — Via nullddb subpackage
  • Zero Dependencies — Core package uses only the standard library

Installation

go get github.com/bjaus/null

For DynamoDB support:

go get github.com/bjaus/null/nullddb

The Problem

Go can't distinguish between "field not provided" and "field explicitly set to null":

type Request struct {
    Name *string `json:"name"`
}

// Both of these result in Name == nil:
// {"name": null}
// {}

This matters for PATCH APIs where you need to know:

  • Unset: Client didn't mention it → don't change it
  • Null: Client wants to clear it → set to NULL
  • Value: Client wants to update it → set to value

Quick Start

package main

import (
    "encoding/json"
    "fmt"

    "github.com/bjaus/null"
)

type UpdateRequest struct {
    Name  null.Value[string] `json:"name"`
    Email null.Value[string] `json:"email"`
    Age   null.Value[int]    `json:"age"`
}

func main() {
    data := `{"name": "Alice", "email": null}`

    var req UpdateRequest
    json.Unmarshal([]byte(data), &req)

    fmt.Println(req.Name.IsValid())  // true - has value "Alice"
    fmt.Println(req.Email.IsNull())  // true - explicitly null
    fmt.Println(req.Age.IsSet())     // false - not in JSON
}

Usage

Creating Values
// Valid value
name := null.New("Alice")

// Explicit null
name := null.NewNull[string]()

// From pointer (nil becomes null)
name := null.NewPtr(namePtr)

// Zero value is unset
var name null.Value[string]
Checking State
v.IsSet()   // true if null OR valid (was explicitly provided)
v.IsNull()  // true if explicitly null
v.IsValid() // true if has a real value
State IsSet IsNull IsValid
Unset false false false
Null true true false
Valid true false true
Extracting Values
v.Get()          // Returns value or zero value of T
v.GetOr("default") // Returns value or the default
v.Ptr()          // Returns *T or nil
PATCH Request Pattern
func UpdateUser(req UpdateRequest, user *User) {
    if req.Name.IsSet() {
        if req.Name.IsNull() {
            user.Name = nil  // Clear the field
        } else {
            user.Name = req.Name.Ptr()  // Update the field
        }
    }
    // If !req.Name.IsSet(), leave unchanged
}
SQL Integration
type User struct {
    ID   int64
    Name null.Value[string]
}

// Scanning
row.Scan(&user.ID, &user.Name)
// NULL → user.Name.IsNull() == true
// "Alice" → user.Name.Get() == "Alice"

// Inserting
db.Exec("INSERT INTO users (name) VALUES ($1)", user.Name)
// Null/Unset → inserts NULL
// Valid → inserts the value
DynamoDB Integration

Use the nullddb subpackage for DynamoDB:

import "github.com/bjaus/null/nullddb"

type Item struct {
    PK   string                `dynamodbav:"pk"`
    Name nullddb.Value[string] `dynamodbav:"name"`
}

item := Item{
    PK:   "user#123",
    Name: nullddb.New("Alice"),
}

// Works with attributevalue.MarshalMap
av, _ := attributevalue.MarshalMap(item)

Convert between null.Value and nullddb.Value:

// null.Value → nullddb.Value
nv := null.New("Alice")
dv := nullddb.From(nv)

// nullddb.Value embeds null.Value, so all methods work
dv.IsValid()  // true
dv.Get()      // "Alice"

API Reference

Constructors
Function Description
New[T](v) Create a valid Value
NewNull[T]() Create a null Value
NewPtr[T](p) Create from pointer (nil → null)
State Methods
Method Description
IsSet() True if null or valid
IsNull() True if explicitly null
IsValid() True if has a value
State() Returns Unset, Null, or Valid
Value Methods
Method Description
Get() Returns value or zero
GetOr(def) Returns value or default
Ptr() Returns pointer or nil
Supported SQL Types

string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool, time.Time, []byte

Design Decisions

Why not pointers?

  • Can't distinguish unset from null
  • Require heap allocation
  • Awkward for literals (&"hello" doesn't work)

Why not sql.NullString?

  • No unset/null distinction
  • Bad JSON marshaling ({"String": "x", "Valid": true})
  • No generics

Why a state enum instead of two bools?

  • Impossible to have invalid combinations
  • Same memory after padding
  • Clearer semantics

License

MIT License - see LICENSE for details.

Documentation

Overview

Package null provides a generic nullable type that distinguishes between unset, null, and valid values.

null solves a common problem in Go: the inability to distinguish between "field was not provided" and "field was explicitly set to null" when working with JSON APIs, databases, and other data sources.

The Three-State Problem

Consider a PATCH request where a client wants to:

  • Leave a field unchanged (don't include it)
  • Clear a field (set it to null)
  • Update a field (set it to a value)

With standard Go types, you can't distinguish these cases:

type Request struct {
    Name *string `json:"name"` // nil means... unset? or null?
}

With null.Value, you can:

type Request struct {
    Name null.Value[string] `json:"name"`
}

// Client sends: {}
// r.Name.IsSet() == false (field was absent)

// Client sends: {"name": null}
// r.Name.IsSet() == true, r.Name.IsNull() == true

// Client sends: {"name": "Alice"}
// r.Name.IsSet() == true, r.Name.IsValid() == true

// Client sends: {"name": ""}
// r.Name.IsSet() == true, r.Name.IsValid() == true (empty string IS a value)

Quick Start

Creating values:

// Valid value
name := null.New("Alice")

// Explicit null
name := null.NewNull[string]()

// From pointer (nil becomes null)
name := null.NewPtr(namePtr)

// Unset (zero value)
var name null.Value[string]

Checking state:

if name.IsSet() {
    // Field was present (either null or a value)
}

if name.IsNull() {
    // Field was explicitly set to null
}

if name.IsValid() {
    // Field has a real value
}

Extracting values:

v := name.Get()          // Returns zero value if not valid
v := name.GetOr("Bob")   // Returns "Bob" if not valid
p := name.Ptr()          // Returns nil if not valid

JSON Integration

Value[T] implements json.Marshaler and json.Unmarshaler with full three-state support:

type User struct {
    Name  null.Value[string] `json:"name"`
    Email null.Value[string] `json:"email,omitempty"`
}

// Unmarshal distinguishes all three states
json.Unmarshal([]byte(`{"name": "Alice"}`), &u)
// u.Name.IsValid() == true, u.Email.IsSet() == false

json.Unmarshal([]byte(`{"name": null}`), &u)
// u.Name.IsNull() == true

Note: When marshaling, both unset and null values produce "null" in JSON (JSON has no concept of "unset"). Use omitempty to omit unset fields.

SQL Integration

Value[T] implements database/sql.Scanner and database/sql/driver.Valuer:

type User struct {
    ID   int64
    Name null.Value[string]
}

// Scanning NULL from database
row.Scan(&u.ID, &u.Name)
// If column is NULL: u.Name.IsNull() == true
// If column has value: u.Name.IsValid() == true

// Inserting NULL into database
db.Exec("INSERT INTO users (name) VALUES ($1)", null.NewNull[string]())

Supported SQL types: string, int/int8/int16/int32/int64, uint/uint8/uint16/uint32/uint64, float32/float64, bool, time.Time, []byte.

DynamoDB Integration

For DynamoDB support, use the nullddb subpackage which wraps Value[T] with DynamoDB marshaling:

import "github.com/bjaus/null/nullddb"

type Item struct {
    PK   string                `dynamodbav:"pk"`
    Name nullddb.Value[string] `dynamodbav:"name"`
}

item := Item{PK: "123", Name: nullddb.New("Alice")}
av, _ := attributevalue.MarshalMap(item)

The nullddb.Value[T] embeds null.Value[T], so all methods are available. Convert between them with nullddb.From():

apiVal := null.New("Alice")
ddbVal := nullddb.From(apiVal)

State Semantics

Value[T] has exactly three states:

State    | IsSet() | IsNull() | IsValid() | Get()
---------|---------|----------|-----------|-------------
Unset    | false   | false    | false     | zero value
Null     | true    | true     | false     | zero value
Valid    | true    | false    | true      | the value

The zero value of Value[T] is Unset, which is the natural state for struct fields that weren't explicitly initialized.

Common Patterns

PATCH request handling:

func UpdateUser(req UpdateRequest) error {
    if req.Name.IsSet() {
        if req.Name.IsNull() {
            user.Name = nil  // Clear the field
        } else {
            user.Name = req.Name.Ptr()  // Update the field
        }
    }
    // If !req.Name.IsSet(), leave user.Name unchanged
}

Default values:

config := Config{
    Timeout: settings.Timeout.GetOr(30 * time.Second),
    Retries: settings.Retries.GetOr(3),
}

Conditional queries:

var conditions []string
var args []any

if filter.Status.IsSet() {
    if filter.Status.IsNull() {
        conditions = append(conditions, "status IS NULL")
    } else {
        conditions = append(conditions, "status = $1")
        args = append(args, filter.Status.Get())
    }
}

Design Decisions

Why not use pointers (*string)?

  • Pointers can't distinguish between "not set" and "set to nil"
  • Pointers require heap allocation
  • Pointers are awkward for literals: you can't write &"hello"

Why not use sql.NullString and friends?

  • They don't distinguish between unset and null
  • They marshal to {"String": "value", "Valid": true} in JSON
  • No generics (separate type for each primitive)

Why a state enum instead of two bools?

  • Cleaner semantics (impossible to have invalid state combinations)
  • Same memory footprint after struct padding
  • Easier to reason about

Why Get() instead of Value()?

  • Value() conflicts with driver.Valuer interface method
  • Get() is clear and concise
Example
package main

import (
	"encoding/json"
	"fmt"

	"github.com/bjaus/null"
)

func main() {
	type User struct {
		Name  null.Value[string] `json:"name"`
		Email null.Value[string] `json:"email"`
		Age   null.Value[int]    `json:"age"`
	}

	// Simulate a PATCH request with partial data
	jsonData := `{"name": "Alice", "email": null}`

	var user User
	_ = json.Unmarshal([]byte(jsonData), &user)

	fmt.Printf("Name set: %v, value: %q\n", user.Name.IsSet(), user.Name.Get())
	fmt.Printf("Email set: %v, null: %v\n", user.Email.IsSet(), user.Email.IsNull())
	fmt.Printf("Age set: %v\n", user.Age.IsSet())

}
Output:

Name set: true, value: "Alice"
Email set: true, null: true
Age set: false

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type State

type State uint8

State represents the three possible states of a Value.

const (
	// Unset indicates the Value was never set (zero value).
	// This is the default state for uninitialized Values.
	Unset State = iota

	// Null indicates the Value was explicitly set to null.
	Null

	// Valid indicates the Value contains a valid value.
	Valid
)

func (State) String

func (s State) String() string

String returns a string representation of the state.

type Value

type Value[T any] struct {
	// contains filtered or unexported fields
}

Value represents an optional value that distinguishes between unset (absent), null (explicit null), and valid (has value).

The zero value is Unset, meaning the field was never set.

func New

func New[T any](v T) Value[T]

New creates a valid Value containing v.

Example
package main

import (
	"fmt"

	"github.com/bjaus/null"
)

func main() {
	v := null.New("hello")
	fmt.Println(v.IsValid(), v.Get())
}
Output:

true hello

func NewNull

func NewNull[T any]() Value[T]

NewNull creates a Value that is explicitly null.

Example
package main

import (
	"fmt"

	"github.com/bjaus/null"
)

func main() {
	v := null.NewNull[string]()
	fmt.Println(v.IsSet(), v.IsNull(), v.IsValid())
}
Output:

true true false

func NewPtr

func NewPtr[T any](p *T) Value[T]

NewPtr creates a Value from a pointer. If p is nil, returns a null Value. Otherwise returns a valid Value with *p.

Example
package main

import (
	"fmt"

	"github.com/bjaus/null"
)

func main() {
	s := "hello"
	v1 := null.NewPtr(&s)
	v2 := null.NewPtr[string](nil)

	fmt.Println(v1.IsValid(), v1.Get())
	fmt.Println(v2.IsNull())
}
Output:

true hello
true

func (Value[T]) Get

func (v Value[T]) Get() T

Get returns the underlying value. Returns the zero value of T if not valid.

func (Value[T]) GetOr

func (v Value[T]) GetOr(def T) T

GetOr returns the underlying value if valid, otherwise returns def.

Example
package main

import (
	"fmt"

	"github.com/bjaus/null"
)

func main() {
	v := null.NewNull[string]()
	fmt.Println(v.GetOr("default"))
}
Output:

default

func (Value[T]) IsNull

func (v Value[T]) IsNull() bool

IsNull returns true if the Value was explicitly set to null.

func (Value[T]) IsSet

func (v Value[T]) IsSet() bool

IsSet returns true if the Value was explicitly set (either to null or a value). Returns false only for the zero value (Unset state).

func (Value[T]) IsValid

func (v Value[T]) IsValid() bool

IsValid returns true if the Value contains a valid value.

func (Value[T]) MarshalJSON

func (v Value[T]) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. Valid values are marshaled as their JSON representation. Null and unset values are marshaled as null.

func (Value[T]) Ptr

func (v Value[T]) Ptr() *T

Ptr returns a pointer to the value if valid, otherwise nil.

func (*Value[T]) Scan

func (v *Value[T]) Scan(src any) error

Scan implements sql.Scanner for SQL database operations. A nil source results in a Null value.

func (Value[T]) State

func (v Value[T]) State() State

State returns the current state (Unset, Null, or Valid).

func (*Value[T]) UnmarshalJSON

func (v *Value[T]) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. If the JSON value is null, the Value becomes Null. Otherwise, the Value becomes Valid with the unmarshaled value.

Note: This method is only called when the field is present in JSON. If the field is absent, the Value remains Unset (zero value).

func (Value[T]) Value

func (v Value[T]) Value() (driver.Value, error)

Value implements driver.Valuer for SQL database operations. Returns nil for null and unset values.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL