An empty update should be a fairly quiet object. Mine grew two fields after I switched JSON packages.
With the same Go struct and the same zero value, encoding/json produced {}. encoding/json/v2 produced {"count":0,"enabled":false}. Both calls succeeded.
For a PATCH-style endpoint where an absent member means “leave this alone,” that is a change in the request's meaning. The serializer hasn't become confused. The old tags are asking a different question under v2.
Outputs from the executed Go example. The PATCH interpretation is the explicitly defined example contract, not a rule imposed by JSON itself. Image by the author.
Decide what an absent field means
Go's new announcement makes encoding/json/v2 generally available in Go 1.27. [1] I used Go 1.27.3 for this example and ran both packages in the same program.
Here is my small update contract: omitted fields leave the stored value unchanged; present fields replace it. So an explicit zero, false, or empty list must survive serialization.
The original struct uses plain values and omitempty. In v1 that option omits the zero number and false Boolean. In v2 it is based on the encoded JSON value: an empty string, array, object or null can be omitted, while zero and false remain present. [2]
That also means “replace a list with an empty list” needs attention. An omitempty tag can remove the very empty array the receiver needs to see.
Give presence its own representation
For this contract I use pointers for the scalar fields and omitzero for all fields. A nil pointer means absent. A non-nil pointer can carry an explicit zero or false. For the slice, nil means absent and a non-nil empty slice means clear the list.
Save this as patch.go:
package main
import (
"fmt"
v1 "encoding/json"
v2 "encoding/json/v2"
)
type OldPatch struct {
Count int `json:"count,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Labels []string `json:"labels,omitempty"`
}
type Patch struct {
Count *int `json:"count,omitzero"`
Enabled *bool `json:"enabled,omitzero"`
Labels []string `json:"labels,omitzero"`
}
func must(b []byte, err error) string {
if err != nil { panic(err) }
return string(b)
}
func main() {
fmt.Println("old tags, v1:", must(v1.Marshal(OldPatch{})))
fmt.Println("old tags, v2:", must(v2.Marshal(OldPatch{})))
fmt.Println("absent, v2:", must(v2.Marshal(Patch{})))
zero, off := 0, false
p := Patch{Count: &zero, Enabled: &off, Labels: []string{}}
fmt.Println("explicit clear, v2:", must(v2.Marshal(p)))
fmt.Println("explicit clear, v1:", must(v1.Marshal(p)))
}
With Go 1.27.3 installed, run go run patch.go. No external module is needed. My output was:
old tags, v1: {}
old tags, v2: {"count":0,"enabled":false}
absent, v2: {}
explicit clear, v2: {"count":0,"enabled":false,"labels":[]}
explicit clear, v1: {"count":0,"enabled":false,"labels":[]}
The important pair is the empty corrected patch and the explicit-clear patch. They now carry different requests. The corrected type also emits the same result through v1 in this check.
I independently parsed those outputs and applied the declared replace-by-member rule to a synthetic record. The absent patch retained its original values; the explicit patch set the count to zero, disabled the flag and cleared its labels. This is serialization plus a small contract check, not a running HTTP service.
Keep the migration close to the API contract
Frank Chu's 7 Go jsonv2 breaks I verified in Go 1.27 covers the wider behavior changes. [3] The omission example deserves a test at your own endpoint boundary because valid JSON can still express the wrong update.
For this example, I would keep golden outputs for an absent patch, an explicit scalar zero and an empty list. A plain search-and-replace of the import cannot decide those meanings for you.
This representation has a limit: a single pointer does not distinguish every possible “absent versus explicit null” requirement. If your API gives null a separate meaning, model that presence state explicitly. The example only promises the absent and explicit-value cases shown above. I haven't measured a speedup or audited a production API.
References
- Go team, A new Go API for JSON.
- Go standard library, encoding/json/v2 documentation, including
omitemptyandomitzero. - Frank Chu, 7 Go jsonv2 breaks I verified in Go 1.27. Its performance measurement is not reused here.
Asset credits
Original contract diagram drawn from the saved program outputs. The program and synthetic update example are original explanatory material.
AI disclosure
AI was used to draft the article.

Top comments (0)