diff --git a/types/json.go b/types/json.go index 2c95e554..925f40cf 100644 --- a/types/json.go +++ b/types/json.go @@ -40,16 +40,27 @@ func (j JSON[T]) Value() (driver.Value, error) { // Scan implements the Scanner interface. func (j *JSON[T]) Scan(value any) error { + var data []byte switch x := value.(type) { case string: - return json.NewDecoder(bytes.NewBuffer([]byte(x))).Decode(j) + data = []byte(x) case []byte: - return json.NewDecoder(bytes.NewBuffer(x)).Decode(j) + data = x case nil: + var zero T + j.Val = zero return nil default: return fmt.Errorf("cannot scan type %T: %v", value, value) } + // Destinations are reused across rows. Decode separately to avoid mutating + // pointers and slices already copied into previously scanned values. + var decoded T + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&decoded); err != nil { + return err + } + j.Val = decoded + return nil } // UnmarshalJSON implements json.Unmarshaler. diff --git a/types/json_scan_reuse_test.go b/types/json_scan_reuse_test.go new file mode 100644 index 00000000..4a7a3bee --- /dev/null +++ b/types/json_scan_reuse_test.go @@ -0,0 +1,54 @@ +package types + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestJSONScanReusedDestination(t *testing.T) { + type nested struct { + Name string `json:"name"` + Tags []string `json:"tags"` + } + type record struct { + Nested *nested `json:"nested"` + } + var destination JSON[record] + if err := destination.Scan([]byte(`{"nested":{"name":"First","tags":["a","b"]}}`)); err != nil { + t.Fatal(err) + } + previous := destination.Val + if err := destination.Scan(`{"nested":{"name":"Second","tags":["c"]}}`); err != nil { + t.Fatal(err) + } + want := record{Nested: &nested{Name: "First", Tags: []string{"a", "b"}}} + if diff := cmp.Diff(want, previous); diff != "" { + t.Fatalf("previous row changed (-want +got):\n%s", diff) + } + if err := destination.Scan([]byte(`{}`)); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(record{}, destination.Val); diff != "" { + t.Fatalf("absent fields retained (-want +got):\n%s", diff) + } + if err := destination.Scan([]byte(`{"nested":{"name":"First"}}`)); err != nil { + t.Fatal(err) + } + previous = destination.Val + if err := destination.Scan([]byte(`{"nested":{"name":"Wrong","tags":123}}`)); err == nil { + t.Fatal("expected invalid JSON field error") + } + if diff := cmp.Diff(previous, destination.Val); diff != "" { + t.Fatalf("failed scan changed destination (-want +got):\n%s", diff) + } + if destination.Val.Nested.Name != "First" { + t.Fatal("failed scan mutated previous row") + } + if err := destination.Scan(nil); err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(record{}, destination.Val); diff != "" { + t.Fatalf("nil scan retained fields (-want +got):\n%s", diff) + } +}