Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions types/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions types/json_scan_reuse_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading