Skip to content

The Checkbox That Silently Killed a Feature in a Go Backend

Adityo Guni Waluyo

An unchecked checkbox sends nothing, and a Go backend reads that silence as false. Lessons from a silent admin-form bug.

TL;DR

Leaving checkboxes unchecked sent nothing, which the backend misread as disabling the features. In Go, a missing boolean unmarshals to false, so absent was treated as off and omitempty hid the problem. The fix was to always send explicit true or false and use null versus absent for nullable fields.

The Checkbox That Silently Killed a Feature

I was adding a new feature to the WisataKota portal admin CMS. Two boolean fields needed to go into the entity editor: rating_enabled defaulting to true, and rental_enabled defaulting to false. At first I just dropped in plain checkboxes in the React form, thinking it was perfectly safe. My logic back then was simple. If the user never touches a checkbox, the browser sends nothing for it in the JSON body. I assumed the backend would read that absence as "no change", leaving the old database value intact.

That assumption was completely wrong.

When I submitted the form with an unchecked box, the rating and rental features on the public page suddenly died. Confused, I checked the network payload in the browser. Sure enough, an unchecked HTML checkbox sends no data at all [3]. The real problem was how the Go backend read that silence.

Why Go Turns Silence Into False

This is where the Go boolean zero value really bites. When Go runs json.Unmarshal, it only allocates the structures actually present in the JSON. If a boolean field is absent from the payload, Go never leaves it "empty" or "null". It hands the field its zero value straight away. And for booleans, that zero value is false [2].

So when the form skips rental_enabled, the Go backend does not think "the user didn't change this, leave it alone". It thinks "this is a boolean, its default is false". The rental feature quietly dies during save.

It gets trickier once you are used to the omitempty tag on Go structs. The tag is handy for shrinking JSON by dropping fields considered empty. But for booleans, false counts as "empty" to omitempty, so the field gets dropped during marshal [1]. It is the classic trap that makes developers think omitting is safe, when it actually destroys important context.

The Fix: Explicit Beats Clever

Once I saw the pattern, I could no longer rely on "not sent means unchanged". I had to make the frontend-backend conversation more transparent.

First, for boolean fields like rating_enabled and rental_enabled, I changed the React logic to always send an explicit value, true or false. No more "absent" or undefined option. I also left a comment in the code reminding the team: "A Go bool that gets omitted decodes as false — send it explicitly so rating/rental never die silently on save".

Second, for fields that are genuinely allowed to be empty like type_id (number or null), I applied RFC 7396 merge-patch semantics [4]. The rule is simple and consistent. A null value means delete that key from the database, while an absent key means leave it alone. So in edit mode I echo the existing type_id value back into the form. In create mode I leave the field out of the payload, letting the server store it as NULL per the master-data design.

This experience changed how I look at form data. Sending data explicitly feels noisier in the network payload, but it beats letting the language guess my intent from missing data.

Sources

Related articles