Skip to content

Instantly share code, notes, and snippets.

@chrismarget
Created March 15, 2026 02:39
Show Gist options
  • Select an option

  • Save chrismarget/b6a05299e88c5944fa18ea5a27006cfd to your computer and use it in GitHub Desktop.

Select an option

Save chrismarget/b6a05299e88c5944fa18ea5a27006cfd to your computer and use it in GitHub Desktop.
Go JSON marshaling with special handling for a single attribute via type aliasing and embedding
package main
import (
"encoding/json"
"fmt"
)
var _ json.Marshaler = (*Thing)(nil) // compile time assertion/check
type Thing struct {
ID string `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
}
// MarshalJSON implements json.Marshaler for Thing, with special handling for the Tags element.
// If Tags is nil, the "tags" attribute will be omitted from the JSON output.
// If Tags is an empty slice, the "tags" attribute will be sent with a value of null.
// If Tags is a non-empty slice, "tags" will be marshaled as normal.
func (t Thing) MarshalJSON() ([]byte, error) {
var tags []byte
switch {
case t.Tags == nil: // omit tags field
case len(t.Tags) == 0: // send `null`
tags = []byte("null")
default: // send tags as normal
// I'm like 60% sure marshaling a string slice cannot error.
tags, _ = json.Marshal(t.Tags)
}
// Alias doesn't implement json.Marshaler, even though Thing does,
// so it won't cause infinite recursion when we call json.Marshal
// on the anonymous struct which embeds it.
type Alias Thing
return json.Marshal(struct {
Tags json.RawMessage `json:"tags,omitempty"`
Alias
}{
Tags: tags,
Alias: Alias(t),
})
}
func main() {
for _, t := range []Thing{
{
ID: "1",
Name: "Two Tags",
Tags: []string{"a", "b"},
},
{
ID: "2",
Name: "Zero Tags",
Tags: []string{},
},
{
ID: "3",
Name: "Nil Tags",
},
} {
b, err := json.Marshal(t)
if err != nil {
panic(err)
}
fmt.Println(string(b))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment