Free tools Windows power users keep installed
One-click scans. No signup required.
To test a Go PATCH endpoint correctly, first establish which patch format and media type it accepts. Then test omitted fields, explicit null, valid replacements, and invalid input separately—and assert both the HTTP response and the resource’s final state. A plain Go pointer field often cannot distinguish an omitted member from one explicitly set to null.
Start with the endpoint’s patch contract
HTTP PATCH does not prescribe one universal meaning for a JSON body. RFC 5789 defines PATCH as applying changes described in a patch document; the document’s media type identifies its format. Check the endpoint documentation and Content-Type handling before deciding what a test should expect. An endpoint can advertise supported formats with Accept-Patch. RFC 5789
The API contract—not a generic testing rule—determines whether a particular null, malformed value, or unknown member is accepted, what status and error body it returns, and whether unknown members are ignored or rejected.
Why a Go pointer does not always distinguish missing from null
With legacy encoding/json struct decoding, an omitted JSON member leaves the destination field unchanged. Explicit JSON null sets pointers, maps, slices, and interfaces to nil; for most other Go types, null has no effect and does not itself produce an error. Consequently, decoding a fresh request into a struct with Name *string can leave Name nil both when name is absent and when it is explicitly null. Go encoding/json documentation
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors#1 Best Overall
If absence means “leave unchanged” but null means “clear,” preserve member presence explicitly. One option is a wrapper with a presence flag and custom UnmarshalJSON; another is decoding the object into raw members and checking whether the key exists before decoding its value. Test the exact decoder, Go version, and options your service uses: other JSON packages or newer APIs may behave differently.
A presence-aware field wrapper
This wrapper records whether the field appeared, while its pointer distinguishes null from a decoded string:
type OptionalString struct {
Present bool
Value *string
}
func (o *OptionalString) UnmarshalJSON(data []byte) error {
o.Present = true
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
o.Value = nil
return nil
}
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
o.Value = &value
return nil
}
type PatchRequest struct {
Name OptionalString `json:"name"`
}
With a newly initialized request value, Present == false means omitted; Present == true with Value == nil means explicit null; and a non-nil value means a decoded string. The string decode rejects values of incompatible JSON types. If request objects may be reused, reset them before decoding so the presence flag cannot carry over from an earlier decode.
Test the three decoded states directly
A small decoder-level test isolates presence semantics from routing and persistence. These examples assume the wrapper above and the standard library decoder:
func TestPatchNamePresence(t *testing.T) {
tests := []struct {
name string
body string
wantPresent bool
wantValue *string
wantErr bool
}{
{name: "omitted", body: `{}`, wantPresent: false},
{name: "null", body: `{"name":null}`, wantPresent: true},
{name: "value", body: `{"name":"Ada"}`, wantPresent: true, wantValue: ptr("Ada")},
{name: "wrong type", body: `{"name":42}`, wantPresent: true, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got PatchRequest
err := json.Unmarshal([]byte(tt.body), &got)
if (err != nil) != tt.wantErr {
t.Fatalf("Unmarshal error = %v, wantErr %v", err, tt.wantErr)
}
if got.Name.Present != tt.wantPresent {
t.Fatalf("Present = %v, want %v", got.Name.Present, tt.wantPresent)
}
if tt.wantErr {
return
}
if !reflect.DeepEqual(got.Name.Value, tt.wantValue) {
t.Fatalf("Value = %v, want %v", got.Name.Value, tt.wantValue)
}
})
}
}
For example, ptr can be a test helper that returns the address of a string. The test checks the decoded representation before update logic interprets it; separately test whether the endpoint maps null to clearing, rejection, or another documented outcome.
Exercise the actual handler with table-driven requests
Use httptest.NewRequest and httptest.NewRecorder to send requests through the handler’s normal routing, decoding, validation, and update path. Set the media type the endpoint actually supports rather than assuming that every PATCH endpoint accepts application/json. Go documents httptest.NewRequest for creating a request to a server handler. Go net/http/httptest documentation
Rank #4
Seed a resource with nonzero values before each case. That makes it possible to see whether omission preserves an existing value and whether a rejected update left state untouched.
| Case | Example body | Assertions to make |
|---|---|---|
| Field omitted | {} |
Check whether the existing value is preserved and assert the status required by the endpoint contract. |
| Explicit null | {"name":null} |
Check whether null clears, removes, is rejected, or has another documented effect. |
| Valid replacement | {"name":"Ada"} |
Assert success and the resulting value. |
| Wrong JSON type | {"name":42} |
Assert the documented rejection or coercion policy; if rejected, verify state did not change. |
| Malformed JSON | {"name": |
Assert the endpoint’s client-error response and unchanged state on rejection. |
| Domain-invalid value | {"age":-1} |
Assert the validation response and unchanged state on rejection. |
| Unknown member | {"typo":true} |
Assert the API’s documented reject-or-ignore policy. |
These are useful test inputs, not universal expected status codes. Encode the endpoint’s actual response contract in each case, including any structured error body.
Best Value
Example handler-test shape
func TestPatchHandler(t *testing.T) {
tests := []struct {
name string
body string
wantStatus int
wantName string
}{
// Populate expected status and final state from this API's contract.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := newStoreWithResource("Grace")
handler := newHandler(store)
req := httptest.NewRequest(
http.MethodPatch,
"/users/1",
strings.NewReader(tt.body),
)
req.Header.Set("Content-Type", "application/merge-patch+json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String())
}
if got := store.Get("1").Name; got != tt.wantName {
t.Fatalf("stored name = %q, want %q", got, tt.wantName)
}
})
}
}
The example uses Merge Patch’s media type only to illustrate setting a request header. Substitute the actual format and expected status, response, and state for your endpoint. In production-quality tests, check the response body as well as the status and stored value.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Verify rejected patches do not partially apply
PATCH application is atomic under RFC 5789: a server must not expose a partially applied patch if the complete patch cannot be applied. For invalid input, inspect the resource after the request, not just the error response. A request that changes one field before failing validation on another should leave the resource in its original state. RFC 5789
For a reliable failure test, seed multiple fields, submit a patch that would alter one field but makes another invalid, then assert the error response and compare the complete resulting resource with the original. This catches update code that mutates a stored object incrementally before validation finishes.
Do not confuse JSON Merge Patch with JSON Patch
The word “PATCH” names the HTTP method; it does not determine whether null means removal. Choose tests based on the document format the endpoint accepts.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →| Format | Media type | How to interpret null and changes |
|---|---|---|
| JSON Merge Patch | application/merge-patch+json |
Object members in the patch add or replace values; a member set to null removes that member from the target. A non-object patch replaces the whole target. This format is not suitable when explicit JSON null must be stored as a meaningful member value. RFC 7396 |
| JSON Patch | application/json-patch+json |
An ordered array of operations such as add, remove, replace, move, copy, and test. A null inside an operation’s value is data, not Merge Patch’s instruction to remove an object member. Include failing operations in tests and verify atomic application. RFC 6902 |
Merge Patch fits object-shaped additions, replacements, and removals. JSON Patch makes ordered operations explicit, which can be useful for array edits or when null must remain a value. The format choice affects client behavior and the tests you should write; it should be explicit in the API contract and media-type handling.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




