Generate Go Test Cases From JSON Files Stored on Disk
Drive Go table tests from a JSON file in testdata: decode into a case struct, run each as a subtest by name, and add cases without touching Go code.

A table test in Go is usually a slice of structs in the test file. That works until the table is large, or until people who are not writing Go want to add cases: a support engineer with a list of inputs that broke last week, an analyst with sample documents, a product owner with edge cases for a pricing rule. For them, a JSON file is a better format than a slice literal.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
package main
type Case struct {
Name string `json:"name"`
Items []int `json:"items"`
Want int `json:"want"`
}
func Total(items []int) int {
t := 0
for _, v := range items {
t += v
}
return t
}
func main() {}package main
import (
"encoding/json"
"os"
"testing"
)
func TestTotalFromJSON(t *testing.T) {
raw, err := os.ReadFile("testdata/total.json")
if err != nil {
t.Fatal(err)
}
var cases []Case
if err := json.Unmarshal(raw, &cases); err != nil {
t.Fatal(err)
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
if got := Total(c.Items); got != c.Want {
t.Errorf("Total(%v) = %d, want %d", c.Items, got, c.Want)
}
})
}
}go test -count=1 -v . 2>&1 | grep -vE "^(=== RUN|=== PAUSE|=== CONT|PASS|ok)" | sed -E "s/ \([0-9.]+s\)//; s#^ +[a-z_]+\.go:[0-9]+: # #" open testdata/total.json: no such file or directory
--- FAIL: TestTotalFromJSON
FAIL
FAIL example.test/lab 0.232s
FAILHow it works
The test reads testdata/total.json, decodes it into []Case with encoding/json, and loops. Each case runs as a subtest named from its name, so the output lists them individually and go test -run 'TestTotalFromJSON/negative' runs one. The JSON has four entries, one of them wrong on purpose, and the log shows three passes and one failure: Total([1 1]) = 2, want 3.
The Case struct carries JSON tags, so the file's field names are part of the format. Decode errors fail the test at once with t.Fatal, because a malformed fixture would otherwise run zero cases and pass. A guard worth adding is if len(cases) == 0 { t.Fatal("no cases") }, for the same reason.
What the failure looks like
A failing case prints the case's name in the subtest header, then your message. With plain table tests, people sometimes forget to include the input in the message and end up counting rows. Here the message includes the items and both numbers, and the name identifies the row. Make that a rule for data driven tests: the message must contain enough to reproduce the case without opening the file. When the data is large, print an index or an identifier from the case instead, such as a ticket number in the name.
The flow for fixing a reported bug becomes short. Add a case with the failing input and the correct expected value, run the test and watch that one case fail, fix the code, and watch it pass. The new case stays in the file as a regression test, and the diff is a handful of lines of data.
Good habits
Keep the file next to the test in testdata. Give every case a unique, descriptive name, since it is the failure label. Use DisallowUnknownFields on the decoder so a typo like "wnat" is an error and not a case with a zero expectation. For inputs that are not valid JSON, such as binary data, store files and list their names in the table; the golden files article covers that layout.
The trade-off is that the file has no type checking, no comments and no shared helper values. Go tables win when cases need computed values or function fields. JSON wins when the cases are data. The format also travels: the same file can drive a test in another language, which is useful when two implementations must agree. The decoding side is in the encoding/json reference, and the base pattern is described in table-driven tests. The testing guide shows where it fits.


