keepcalculator

06 Numbers and Powers

Go JSON Patch: Handling Null, Undefined, and Zero

Card No. 06·49534 · Updated · Standard formula, shown below

This guide explains how to model null, undefined (missing), and zero values in JSON Patch requests when working with Go. You’ll learn how to distinguish between a field that is absent, explicitly set to null, and one set to 0, so your API updates behave exactly as intended. We’ll cover the use of pointers, custom types, and the omitempty tag, along with practical examples and common pitfalls.

What You’ll Need

  • Go 1.16 or later (for any type support, though older versions work with interface{})
  • A basic understanding of JSON and JSON Patch (RFC 6902) operations: add, remove, replace, copy, move, test
  • Familiarity with Go’s encoding/json package
  • Optionally, a JSON Patch library like evanphx/json-patch (but the concepts apply to any implementation)

Step-by-Step

  1. Understand the three states. In JSON, a field can be:
    • Undefined: the key is absent from the JSON object. In Go, this is the zero value of the type (e.g., 0 for int, "" for string).
    • Null: the key exists with value null.
    • Zero: the key exists with a value like 0, "", false, etc.
  2. Use pointers for nullable fields. In your Go struct, define fields that can be null as pointers (*int, *string, etc.). A nil pointer means null in JSON; a non-nil pointer means the value is present. Example:
    type User struct {
        Name  *string `json:"name"`
        Age   *int    `json:"age"`
        Email string  `json:"email"`
    }

    Here, Name and Age can be null, while Email cannot (it will be an empty string if missing).

  3. Distinguish undefined from null. A pointer field that is nil will be serialized as null by default. If you need to distinguish “not provided” from “explicitly null”, you need a custom type or a separate presence flag. For JSON Patch, however, you usually don’t need this distinction on the receiving side—the patch operation itself tells you what to do. For example, a remove operation means “delete the key”, while a replace with null means “set to null”.
  4. Handle zero values correctly. In Go, a non-pointer int field will be 0 if not set. If you want to allow a real 0 but also detect “not provided”, use a pointer. For JSON Patch, a replace operation with 0 should set the field to 0, not leave it unchanged. Always use pointers for numeric fields that can legitimately be zero.
  5. Use omitempty carefully. The omitempty tag omits a field when it has its zero value. For pointers, omitempty omits when the pointer is nil. This is useful for requests where you don’t want to send unchanged fields. But beware: if you use omitempty on a non-pointer int, a value of 0 will be omitted, which may be incorrect for your API. Example:
    type UpdateRequest struct {
        Age *int `json:"age,omitempty"`
    }

    This omits age if it’s nil, but includes it if it’s 0 (because the pointer is non-nil).

  6. Build JSON Patch operations explicitly. When constructing a patch, you decide what each operation means. For example:
    • To set a field to null: use {"op":"replace","path":"/age","value":null}
    • To remove a field entirely: use {"op":"remove","path":"/age"}
    • To set a field to 0: use {"op":"replace","path":"/age","value":0}

    In Go, you can define a struct for the operation:

    type PatchOperation struct {
        Op    string      `json:"op"`
        Path  string      `json:"path"`
        Value interface{} `json:"value,omitempty"`
    }

    Note: omitempty on Value will omit it when nil, which is fine for remove operations. For replace with null, you need to pass a typed nil like nil (which is an untyped nil) or a pointer to nil. Actually, interface{} holding a nil pointer will serialize as null—that works.

  7. Test your serialization. Write unit tests to verify that your structs marshal to the expected JSON. For example:
    func TestPatch(t *testing.T) {
        op := PatchOperation{Op: "replace", Path: "/age", Value: nil}
        b, _ := json.Marshal(op)
        // b should be: {"op":"replace","path":"/age","value":null}
    }

    If Value is a typed nil pointer, it will marshal as null. But if you use omitempty, a nil pointer will be omitted, so for replace operations you should not use omitempty on Value.

  8. Decode incoming patches correctly. When receiving a JSON Patch, unmarshal into a slice of PatchOperation. The Value field will be nil for null, and 0 for zero. You can then apply the patch to your data structure, handling each operation type.
  9. Apply the patch to your data. For each operation, update your target struct. For replace with null, set the pointer field to nil. For replace with 0, set the pointer to a value pointing to 0. For remove, delete the key (if using a map) or set the pointer to nil (if using a struct).

Common Mistakes

  • Using non-pointer types for nullable fields. If you use int instead of *int, you cannot represent null—it will always be 0.
  • Misusing omitempty on value types. This causes zero values to be omitted, making it impossible to send a real 0 in a patch.
  • Confusing null with 0. In JSON, null is not the same as 0. Your API logic must treat them differently.
  • Assuming remove is the same as setting null. remove deletes the key; replace with null keeps the key but sets it to null.
  • Not handling pointer nil when applying patches. If you set a pointer field to nil, make sure your code doesn’t dereference it later.

Tips

  • Use pointers for all fields that can be null or zero. This gives you the most flexibility.
  • Consider a custom type for tri-state fields. If you need to distinguish “not provided”, “null”, and “zero”, create a type like type NullableInt struct { Valid bool; Value int } and implement MarshalJSON and UnmarshalJSON.
  • Keep your patch operations explicit. Don’t rely on the zero value of a struct to mean “no change”. Always build operations deliberately.
  • Use a library for applying patches. Libraries like evanphx/json-patch handle the details, but you still need to model your data correctly.
  • Document your API’s semantics. Clearly state whether a missing field means “no change” or “set to zero”. This avoids client confusion.

Troubleshooting

  • Patch value is omitted when sending null. If you use omitempty on the Value field, a nil value will be omitted. Remove omitempty from Value or use a pointer type for Value (like *interface{}) to force inclusion.
  • Cannot distinguish between missing and null in request body. If you’re not using JSON Patch but a direct JSON body, use pointers and check for nil. For missing fields, the pointer will be nil as well, so you need a separate presence flag or a custom type.
  • Zero value is omitted from patch. If you use omitempty on a non-pointer int, 0 is omitted. Use a pointer or remove omitempty.
  • Applying a patch with null to a non-pointer field. This will cause an error because you can’t set an int to null. Ensure the target field is a pointer.
  • JSON unmarshal of null into a pointer field. This works fine—it sets the pointer to nil. But if you unmarshal into a non-pointer, it returns an error.

Frequently Asked Questions

How do I represent “field not provided” vs “field set to null” in a JSON Patch?

In JSON Patch, you don’t need to distinguish them on the wire. The operation itself defines the intent: remove means “not present”, replace with null means “present but null”. Your server logic should handle each operation accordingly.

Should I use omitempty on patch operation value?

No, not if you need to send null. omitempty will drop nil values. For remove operations, you don’t need a value, so it’s fine to omit it. But for replace with null, you must include it. A common pattern is to use a pointer to interface{} for the value and omit omitempty.

Can I use json.RawMessage for the value field?

Yes, json.RawMessage preserves the exact JSON, including null. It’s useful if you need to forward the raw value. However, it requires manual unmarshalling when applying the patch.

How do I apply a JSON Patch to a map instead of a struct?

You can unmarshal the target JSON into a map[string]interface{}. Then, for replace with null, set the map key to nil. For remove, delete the key. This approach is simpler for dynamic data but loses type safety.

What’s the difference between null and 0 in JSON?

null means “no value”, while 0 is a numeric value. In Go, nil for a pointer maps to null, and 0 for an integer maps to 0. They are distinct and should be treated differently in your business logic.

Is it better to use a custom type or pointers?

Pointers are simpler and cover most cases. Custom types are needed only when you must distinguish between three states (missing, null, zero) in a single field, which is rare in JSON Patch because the operation type already provides that distinction.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →