github.com/jackc/pgx/v5@v5.5.5/pgtype/json_test.go (about) 1 package pgtype_test 2 3 import ( 4 "context" 5 "database/sql" 6 "database/sql/driver" 7 "encoding/json" 8 "errors" 9 "testing" 10 11 pgx "github.com/jackc/pgx/v5" 12 "github.com/jackc/pgx/v5/pgxtest" 13 "github.com/stretchr/testify/require" 14 ) 15 16 func isExpectedEqMap(a any) func(any) bool { 17 return func(v any) bool { 18 aa := a.(map[string]any) 19 bb := v.(map[string]any) 20 21 if (aa == nil) != (bb == nil) { 22 return false 23 } 24 25 if aa == nil { 26 return true 27 } 28 29 if len(aa) != len(bb) { 30 return false 31 } 32 33 for k := range aa { 34 if aa[k] != bb[k] { 35 return false 36 } 37 } 38 39 return true 40 } 41 } 42 43 func TestJSONCodec(t *testing.T) { 44 type jsonStruct struct { 45 Name string `json:"name"` 46 Age int `json:"age"` 47 } 48 49 pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, nil, "json", []pgxtest.ValueRoundTripTest{ 50 {nil, new(*jsonStruct), isExpectedEq((*jsonStruct)(nil))}, 51 {map[string]any(nil), new(*string), isExpectedEq((*string)(nil))}, 52 {map[string]any(nil), new([]byte), isExpectedEqBytes([]byte(nil))}, 53 {[]byte(nil), new([]byte), isExpectedEqBytes([]byte(nil))}, 54 {nil, new([]byte), isExpectedEqBytes([]byte(nil))}, 55 56 // Test sql.Scanner. (https://github.com/jackc/pgx/issues/1418) 57 {"42", new(sql.NullInt64), isExpectedEq(sql.NullInt64{Int64: 42, Valid: true})}, 58 59 // Test driver.Valuer. (https://github.com/jackc/pgx/issues/1430) 60 {sql.NullInt64{Int64: 42, Valid: true}, new(sql.NullInt64), isExpectedEq(sql.NullInt64{Int64: 42, Valid: true})}, 61 62 // Test driver.Valuer is used before json.Marshaler (https://github.com/jackc/pgx/issues/1805) 63 {Issue1805(7), new(Issue1805), isExpectedEq(Issue1805(7))}, 64 }) 65 66 pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{ 67 {[]byte("{}"), new([]byte), isExpectedEqBytes([]byte("{}"))}, 68 {[]byte("null"), new([]byte), isExpectedEqBytes([]byte("null"))}, 69 {[]byte("42"), new([]byte), isExpectedEqBytes([]byte("42"))}, 70 {[]byte(`"hello"`), new([]byte), isExpectedEqBytes([]byte(`"hello"`))}, 71 {[]byte(`"hello"`), new(string), isExpectedEq(`"hello"`)}, 72 {map[string]any{"foo": "bar"}, new(map[string]any), isExpectedEqMap(map[string]any{"foo": "bar"})}, 73 {jsonStruct{Name: "Adam", Age: 10}, new(jsonStruct), isExpectedEq(jsonStruct{Name: "Adam", Age: 10})}, 74 }) 75 } 76 77 type Issue1805 int 78 79 func (i *Issue1805) Scan(src any) error { 80 var source []byte 81 switch src.(type) { 82 case string: 83 source = []byte(src.(string)) 84 case []byte: 85 source = src.([]byte) 86 default: 87 return errors.New("unknown source type") 88 } 89 var newI int 90 if err := json.Unmarshal(source, &newI); err != nil { 91 return err 92 } 93 *i = Issue1805(newI) 94 return nil 95 } 96 97 func (i Issue1805) Value() (driver.Value, error) { 98 b, err := json.Marshal(int(i)) 99 return string(b), err 100 } 101 102 func (i Issue1805) UnmarshalJSON(bytes []byte) error { 103 return errors.New("UnmarshalJSON called") 104 } 105 106 func (i Issue1805) MarshalJSON() ([]byte, error) { 107 return nil, errors.New("MarshalJSON called") 108 } 109 110 // https://github.com/jackc/pgx/issues/1273#issuecomment-1221414648 111 func TestJSONCodecUnmarshalSQLNull(t *testing.T) { 112 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 113 // Slices are nilified 114 slice := []string{"foo", "bar", "baz"} 115 err := conn.QueryRow(ctx, "select null::json").Scan(&slice) 116 require.NoError(t, err) 117 require.Nil(t, slice) 118 119 // Maps are nilified 120 m := map[string]any{"foo": "bar"} 121 err = conn.QueryRow(ctx, "select null::json").Scan(&m) 122 require.NoError(t, err) 123 require.Nil(t, m) 124 125 m = map[string]interface{}{"foo": "bar"} 126 err = conn.QueryRow(ctx, "select null::json").Scan(&m) 127 require.NoError(t, err) 128 require.Nil(t, m) 129 130 // Pointer to pointer are nilified 131 n := 42 132 p := &n 133 err = conn.QueryRow(ctx, "select null::json").Scan(&p) 134 require.NoError(t, err) 135 require.Nil(t, p) 136 137 // A string cannot scan a NULL. 138 str := "foobar" 139 err = conn.QueryRow(ctx, "select null::json").Scan(&str) 140 require.EqualError(t, err, "can't scan into dest[0]: cannot scan NULL into *string") 141 142 // A non-string cannot scan a NULL. 143 err = conn.QueryRow(ctx, "select null::json").Scan(&n) 144 require.EqualError(t, err, "can't scan into dest[0]: cannot scan NULL into *int") 145 }) 146 } 147 148 // https://github.com/jackc/pgx/issues/1470 149 func TestJSONCodecPointerToPointerToString(t *testing.T) { 150 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 151 var s *string 152 err := conn.QueryRow(ctx, "select '{}'::json").Scan(&s) 153 require.NoError(t, err) 154 require.NotNil(t, s) 155 require.Equal(t, "{}", *s) 156 157 err = conn.QueryRow(ctx, "select null::json").Scan(&s) 158 require.NoError(t, err) 159 require.Nil(t, s) 160 }) 161 } 162 163 // https://github.com/jackc/pgx/issues/1691 164 func TestJSONCodecPointerToPointerToInt(t *testing.T) { 165 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 166 n := 44 167 p := &n 168 err := conn.QueryRow(ctx, "select 'null'::jsonb").Scan(&p) 169 require.NoError(t, err) 170 require.Nil(t, p) 171 }) 172 } 173 174 // https://github.com/jackc/pgx/issues/1691 175 func TestJSONCodecPointerToPointerToStruct(t *testing.T) { 176 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 177 type ImageSize struct { 178 Height int `json:"height"` 179 Width int `json:"width"` 180 Str string `json:"str"` 181 } 182 is := &ImageSize{Height: 100, Width: 100, Str: "str"} 183 err := conn.QueryRow(ctx, `select 'null'::jsonb`).Scan(&is) 184 require.NoError(t, err) 185 require.Nil(t, is) 186 }) 187 } 188 189 func TestJSONCodecClearExistingValueBeforeUnmarshal(t *testing.T) { 190 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 191 m := map[string]any{} 192 err := conn.QueryRow(ctx, `select '{"foo": "bar"}'::json`).Scan(&m) 193 require.NoError(t, err) 194 require.Equal(t, map[string]any{"foo": "bar"}, m) 195 196 err = conn.QueryRow(ctx, `select '{"baz": "quz"}'::json`).Scan(&m) 197 require.NoError(t, err) 198 require.Equal(t, map[string]any{"baz": "quz"}, m) 199 }) 200 } 201 202 type ParentIssue1681 struct { 203 Child ChildIssue1681 204 } 205 206 func (t *ParentIssue1681) MarshalJSON() ([]byte, error) { 207 return []byte(`{"custom":"thing"}`), nil 208 } 209 210 type ChildIssue1681 struct{} 211 212 func (t ChildIssue1681) MarshalJSON() ([]byte, error) { 213 return []byte(`{"someVal": false}`), nil 214 } 215 216 // https://github.com/jackc/pgx/issues/1681 217 func TestJSONCodecEncodeJSONMarshalerThatCanBeWrapped(t *testing.T) { 218 skipCockroachDB(t, "CockroachDB treats json as jsonb. This causes it to format differently than PostgreSQL.") 219 220 defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { 221 var jsonStr string 222 err := conn.QueryRow(context.Background(), "select $1::json", &ParentIssue1681{}).Scan(&jsonStr) 223 require.NoError(t, err) 224 require.Equal(t, `{"custom":"thing"}`, jsonStr) 225 }) 226 }