Skip to content

Three Nullable Shapes in a Go Write Path

Adityo Guni Waluyo

Threading nullable columns through a Go CRUD: the PUT idiom, plain bools, a pointer ID, and an explicit null contract.

TL;DR

KotaPortal’s upsert adds four fields via three patterns: contact person uses empty-string-means-null, two booleans are always required, and type ID uses a nil pointer for null. PUT does a full overwrite, while responses avoid omitempty to ensure nulls appear explicitly for generated clients. New checks cap contact data at 100 characters and require type ID to be at least one.

I was reading a commit diff in a Go service for the KotaPortal admin feature. Four new columns had to pass through the CRUD: contact data, two toggles (rating and rental), plus a type ID. Three data shapes in a single upsert.

My first instinct: make every field a pointer so it can be null, or slap omitempty on every JSON response so the data looks clean. I also briefly assumed that leaving a field out of a PUT request would keep the old value, PATCH-style.

That guess missed the mark in two places. In this repo, PUT means a full write; leaving a field out never preserves the old value. And with omitempty on responses, a client generating code from OpenAPI loses the key entirely, instead of seeing it as null.

First Shape: A String That Follows the Phone Idiom

Contact data is an optional string following the "empty string = clear" idiom. The model uses NullString from the database/sql package, a type designed exactly for nullable columns, complete with a Valid flag as the switch [7]. In the request it arrives as a plain string. When it comes in empty, the Valid flag is set false so the column stores NULL. Exactly how the existing phone column already behaves.

Second and Third Shapes: Plain Bools and a Pointer ID

The rating and rental toggles are plain bools. Both columns are NOT NULL with database defaults, which means every request always carries their value; they are never null. The only interesting difference between them is the default itself: one true, one false.

The type ID is a different animal. It is an int64 pointer in the request and NullInt64 in the model [7]. When the pointer is nil, the system translates it to NULL the same way as the first shape. The difference: no empty-string trick here. Nil is the only way to say empty.

The Response Contract: Explicit Null

On the response side, we use pointers without omitempty. The reason is simple. When a column is cleared, the resulting JSON must still write contact_person null or type_id null, not drop the key. The round-trip integration test locks this down with exact substring asserts. Clients auto-generated from OpenAPI can rely on it, because the key is always there.

The principle matches API conventions: optional fields must not be assumed present by the reader, and defaults are only valid on optional fields [4]. Nullable serialization in Go does require a pointer without omitempty so the null actually appears [4].

// Model: database types that can be NULL
type EntityModel struct {
    ContactPerson sql.NullString
    TypeID        sql.NullInt64
    RatingEnabled bool
}

// Request: plain string for the clear idiom, pointer for the ID
type UpsertRequest struct {
    ContactPerson string
    TypeID        *int64
    RatingEnabled bool
}

// Response: pointer WITHOUT omitempty so null still shows up
type EntityResponse struct {
    ContactPerson *string
    TypeID        *int64
    RatingEnabled bool
}

Two new validations stand at the service entrance: contact data is capped at 100 characters, and the type ID must be at least one because zero is reserved, with no referential check until the master table lands in the next slice.

I picked this pattern because it gives absolute clarity. Clients never have to guess whether data is missing because of an error, was never set, or was cleared on purpose. Explicit null is the most honest answer.

## Sources [7] https://pkg.go.dev/database/sql [4] https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md

Related articles