How to Handle Optional/Nullable JSON Fields in Go

Use pointer types (e.g., `*string`, `*int`) for struct fields to distinguish between a missing JSON key and a zero-value field, or use `json.RawMessage` for dynamic handling.

Use pointer types (e.g., *string, *int) for struct fields to distinguish between a missing JSON key and a zero-value field, or use json.RawMessage for dynamic handling. When unmarshaling, a missing key leaves the pointer as nil, while a present key with a null value also results in nil, but you can differentiate a missing key from an explicit null if you need to by checking the raw JSON or using custom unmarshaling logic.

Here is a practical example using pointers, which is the standard idiomatic approach:

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name    string  `json:"name"`
	Age     *int    `json:"age"`
	Email   *string `json:"email"`
}

func main() {
	// JSON with missing "age" and explicit null "email"
	jsonData := `{"name": "Alice", "email": null}`

	var u User
	err := json.Unmarshal([]byte(jsonData), &u)
	if err != nil {
		panic(err)
	}

	// Check if fields were present or null
	if u.Age == nil {
		fmt.Println("Age is missing or null")
	} else {
		fmt.Printf("Age: %d\n", *u.Age)
	}

	if u.Email == nil {
		fmt.Println("Email is missing or null")
	} else {
		fmt.Printf("Email: %s\n", *u.Email)
	}
}

If you need to distinguish between a key being completely absent versus being explicitly set to null in the JSON, you can implement a custom UnmarshalJSON method. This allows you to inspect the raw bytes before decoding:

type Config struct {
	Timeout *int `json:"timeout"`
}

func (c *Config) UnmarshalJSON(data []byte) error {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	if raw["timeout"] == nil {
		// Key is missing
		c.Timeout = nil
	} else if string(raw["timeout"]) == "null" {
		// Key is present but value is null
		c.Timeout = nil
	} else {
		// Key is present with a value
		var val int
		if err := json.Unmarshal(raw["timeout"], &val); err != nil {
			return err
		}
		c.Timeout = &val
	}
	return nil
}

For marshaling, pointers automatically handle nulls: if a pointer field is nil, the encoding/json package omits the key entirely unless you use the omitempty tag, in which case it omits the key; if you want to explicitly send null, ensure the pointer is nil and do not use omitempty. If you need to send null explicitly even when the value is zero, you must use a pointer and set it to nil rather than a zero value.