Skip to content

Commit 82a76c2

Browse files
committed
optimize JSON unmarshaling
- drops support for unmarshaling from sql.NullXXX JSON objects - unmarshal errors no longer set Valid to false
1 parent e99f90c commit 82a76c2

20 files changed

+323
-334
lines changed

bool.go

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package null
22

33
import (
4+
"bytes"
45
"database/sql"
56
"encoding/json"
67
"errors"
78
"fmt"
8-
"reflect"
99
)
1010

1111
// Bool is a nullable bool.
@@ -46,30 +46,22 @@ func (b Bool) ValueOrZero() bool {
4646
// UnmarshalJSON implements json.Unmarshaler.
4747
// It supports number and null input.
4848
// 0 will not be considered a null Bool.
49-
// It also supports unmarshalling a sql.NullBool.
5049
func (b *Bool) UnmarshalJSON(data []byte) error {
51-
var err error
52-
var v interface{}
53-
if err = json.Unmarshal(data, &v); err != nil {
54-
return err
55-
}
56-
switch x := v.(type) {
57-
case bool:
58-
b.Bool = x
59-
case map[string]interface{}:
60-
err = json.Unmarshal(data, &b.NullBool)
61-
case nil:
50+
if bytes.Equal(data, nullBytes) {
6251
b.Valid = false
6352
return nil
64-
default:
65-
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Bool", reflect.TypeOf(v).Name())
6653
}
67-
b.Valid = err == nil
68-
return err
54+
55+
if err := json.Unmarshal(data, &b.Bool); err != nil {
56+
return fmt.Errorf("null: couldn't unmarshal JSON: %w", err)
57+
}
58+
59+
b.Valid = true
60+
return nil
6961
}
7062

7163
// UnmarshalText implements encoding.TextUnmarshaler.
72-
// It will unmarshal to a null Bool if the input is a blank or not an integer.
64+
// It will unmarshal to a null Bool if the input is blank.
7365
// It will return an error if the input is not an integer, blank, or "null".
7466
func (b *Bool) UnmarshalText(text []byte) error {
7567
str := string(text)
@@ -82,8 +74,7 @@ func (b *Bool) UnmarshalText(text []byte) error {
8274
case "false":
8375
b.Bool = false
8476
default:
85-
b.Valid = false
86-
return errors.New("invalid input:" + str)
77+
return errors.New("null: invalid input for UnmarshalText:" + str)
8778
}
8879
b.Valid = true
8980
return nil

bool_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package null
22

33
import (
44
"encoding/json"
5+
"errors"
56
"testing"
67
)
78

@@ -39,8 +40,9 @@ func TestUnmarshalBool(t *testing.T) {
3940

4041
var nb Bool
4142
err = json.Unmarshal(nullBoolJSON, &nb)
42-
maybePanic(err)
43-
assertBool(t, nb, "sq.NullBool json")
43+
if err == nil {
44+
panic("err should not be nil")
45+
}
4446

4547
var null Bool
4648
err = json.Unmarshal(nullJSON, &null)
@@ -56,8 +58,9 @@ func TestUnmarshalBool(t *testing.T) {
5658

5759
var invalid Bool
5860
err = invalid.UnmarshalJSON(invalidJSON)
59-
if _, ok := err.(*json.SyntaxError); !ok {
60-
t.Errorf("expected json.SyntaxError, not %T", err)
61+
var syntaxError *json.SyntaxError
62+
if !errors.As(err, &syntaxError) {
63+
t.Errorf("expected wrapped json.SyntaxError, not %T", err)
6164
}
6265
}
6366

float.go

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package null
22

33
import (
4+
"bytes"
45
"database/sql"
56
"encoding/json"
7+
"errors"
68
"fmt"
79
"math"
810
"reflect"
@@ -50,37 +52,40 @@ func (f Float) ValueOrZero() float64 {
5052
// UnmarshalJSON implements json.Unmarshaler.
5153
// It supports number and null input.
5254
// 0 will not be considered a null Float.
53-
// It also supports unmarshalling a sql.NullFloat64.
5455
func (f *Float) UnmarshalJSON(data []byte) error {
55-
var err error
56-
var v interface{}
57-
if err = json.Unmarshal(data, &v); err != nil {
58-
return err
56+
if bytes.Equal(data, nullBytes) {
57+
f.Valid = false
58+
return nil
5959
}
60-
switch x := v.(type) {
61-
case float64:
62-
f.Float64 = float64(x)
63-
case string:
64-
str := string(x)
65-
if len(str) == 0 {
66-
f.Valid = false
60+
61+
if err := json.Unmarshal(data, &f.Float64); err != nil {
62+
var typeError *json.UnmarshalTypeError
63+
if errors.As(err, &typeError) {
64+
// special case: accept string input
65+
if typeError.Value != "string" {
66+
return fmt.Errorf("null: JSON input is invalid type (need float or string): %w", err)
67+
}
68+
var str string
69+
if err := json.Unmarshal(data, &str); err != nil {
70+
return fmt.Errorf("null: couldn't unmarshal number string: %w", err)
71+
}
72+
n, err := strconv.ParseFloat(str, 64)
73+
if err != nil {
74+
return fmt.Errorf("null: couldn't convert string to float: %w", err)
75+
}
76+
f.Float64 = n
77+
f.Valid = true
6778
return nil
6879
}
69-
f.Float64, err = strconv.ParseFloat(str, 64)
70-
case map[string]interface{}:
71-
err = json.Unmarshal(data, &f.NullFloat64)
72-
case nil:
73-
f.Valid = false
74-
return nil
75-
default:
76-
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name())
80+
return fmt.Errorf("null: couldn't unmarshal JSON: %w", err)
7781
}
78-
f.Valid = err == nil
79-
return err
82+
83+
f.Valid = true
84+
return nil
8085
}
8186

8287
// UnmarshalText implements encoding.TextUnmarshaler.
83-
// It will unmarshal to a null Float if the input is a blank or not an integer.
88+
// It will unmarshal to a null Float if the input is blank.
8489
// It will return an error if the input is not an integer, blank, or "null".
8590
func (f *Float) UnmarshalText(text []byte) error {
8691
str := string(text)
@@ -90,7 +95,10 @@ func (f *Float) UnmarshalText(text []byte) error {
9095
}
9196
var err error
9297
f.Float64, err = strconv.ParseFloat(string(text), 64)
93-
f.Valid = err == nil
98+
if err != nil {
99+
return fmt.Errorf("null: couldn't unmarshal text: %w", err)
100+
}
101+
f.Valid = true
94102
return err
95103
}
96104

float_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package null
22

33
import (
44
"encoding/json"
5+
"errors"
56
"math"
67
"testing"
78
)
@@ -46,8 +47,9 @@ func TestUnmarshalFloat(t *testing.T) {
4647

4748
var nf Float
4849
err = json.Unmarshal(nullFloatJSON, &nf)
49-
maybePanic(err)
50-
assertFloat(t, nf, "sql.NullFloat64 json")
50+
if err == nil {
51+
panic("expected error")
52+
}
5153

5254
var null Float
5355
err = json.Unmarshal(nullJSON, &null)
@@ -56,20 +58,21 @@ func TestUnmarshalFloat(t *testing.T) {
5658

5759
var blank Float
5860
err = json.Unmarshal(floatBlankJSON, &blank)
59-
maybePanic(err)
60-
assertNullFloat(t, blank, "null blank string json")
61+
if err == nil {
62+
panic("expected error")
63+
}
6164

6265
var badType Float
6366
err = json.Unmarshal(boolJSON, &badType)
6467
if err == nil {
6568
panic("err should not be nil")
6669
}
67-
assertNullFloat(t, badType, "wrong type json")
6870

6971
var invalid Float
7072
err = invalid.UnmarshalJSON(invalidJSON)
71-
if _, ok := err.(*json.SyntaxError); !ok {
72-
t.Errorf("expected json.SyntaxError, not %T", err)
73+
var syntaxError *json.SyntaxError
74+
if !errors.As(err, &syntaxError) {
75+
t.Errorf("expected wrapped json.SyntaxError, not %T", err)
7376
}
7477
}
7578

@@ -88,6 +91,12 @@ func TestTextUnmarshalFloat(t *testing.T) {
8891
err = null.UnmarshalText([]byte("null"))
8992
maybePanic(err)
9093
assertNullFloat(t, null, `UnmarshalText() "null"`)
94+
95+
var invalid Float
96+
err = invalid.UnmarshalText([]byte("hello world"))
97+
if err == nil {
98+
panic("expected error")
99+
}
91100
}
92101

93102
func TestMarshalFloat(t *testing.T) {

int.go

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package null
22

33
import (
4+
"bytes"
45
"database/sql"
56
"encoding/json"
7+
"errors"
68
"fmt"
7-
"reflect"
89
"strconv"
910
)
1011

@@ -47,40 +48,42 @@ func (i Int) ValueOrZero() int64 {
4748
}
4849

4950
// UnmarshalJSON implements json.Unmarshaler.
50-
// It supports number and null input.
51+
// It supports number, string, and null input.
5152
// 0 will not be considered a null Int.
52-
// It also supports unmarshalling a sql.NullInt64.
5353
func (i *Int) UnmarshalJSON(data []byte) error {
54-
var err error
55-
var v interface{}
56-
if err = json.Unmarshal(data, &v); err != nil {
57-
return err
54+
if bytes.Equal(data, nullBytes) {
55+
i.Valid = false
56+
return nil
5857
}
59-
switch x := v.(type) {
60-
case float64:
61-
// Unmarshal again, directly to int64, to avoid intermediate float64
62-
err = json.Unmarshal(data, &i.Int64)
63-
case string:
64-
str := string(x)
65-
if len(str) == 0 {
66-
i.Valid = false
58+
59+
if err := json.Unmarshal(data, &i.Int64); err != nil {
60+
var typeError *json.UnmarshalTypeError
61+
if errors.As(err, &typeError) {
62+
// special case: accept string input
63+
if typeError.Value != "string" {
64+
return fmt.Errorf("null: JSON input is invalid type (need int or string): %w", err)
65+
}
66+
var str string
67+
if err := json.Unmarshal(data, &str); err != nil {
68+
return fmt.Errorf("null: couldn't unmarshal number string: %w", err)
69+
}
70+
n, err := strconv.ParseInt(str, 10, 64)
71+
if err != nil {
72+
return fmt.Errorf("null: couldn't convert string to int: %w", err)
73+
}
74+
i.Int64 = n
75+
i.Valid = true
6776
return nil
6877
}
69-
i.Int64, err = strconv.ParseInt(str, 10, 64)
70-
case map[string]interface{}:
71-
err = json.Unmarshal(data, &i.NullInt64)
72-
case nil:
73-
i.Valid = false
74-
return nil
75-
default:
76-
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Int", reflect.TypeOf(v).Name())
78+
return fmt.Errorf("null: couldn't unmarshal JSON: %w", err)
7779
}
78-
i.Valid = err == nil
79-
return err
80+
81+
i.Valid = true
82+
return nil
8083
}
8184

8285
// UnmarshalText implements encoding.TextUnmarshaler.
83-
// It will unmarshal to a null Int if the input is a blank or not an integer.
86+
// It will unmarshal to a null Int if the input is blank.
8487
// It will return an error if the input is not an integer, blank, or "null".
8588
func (i *Int) UnmarshalText(text []byte) error {
8689
str := string(text)
@@ -90,8 +93,11 @@ func (i *Int) UnmarshalText(text []byte) error {
9093
}
9194
var err error
9295
i.Int64, err = strconv.ParseInt(string(text), 10, 64)
93-
i.Valid = err == nil
94-
return err
96+
if err != nil {
97+
return fmt.Errorf("null: couldn't unmarshal text: %w", err)
98+
}
99+
i.Valid = true
100+
return nil
95101
}
96102

97103
// MarshalJSON implements json.Marshaler.

int_test.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package null
22

33
import (
44
"encoding/json"
5+
"errors"
56
"math"
67
"strconv"
78
"testing"
@@ -46,13 +47,15 @@ func TestUnmarshalInt(t *testing.T) {
4647

4748
var ni Int
4849
err = json.Unmarshal(nullIntJSON, &ni)
49-
maybePanic(err)
50-
assertInt(t, ni, "sql.NullInt64 json")
50+
if err == nil {
51+
panic("err should not be nill")
52+
}
5153

5254
var bi Int
5355
err = json.Unmarshal(floatBlankJSON, &bi)
54-
maybePanic(err)
55-
assertNullInt(t, bi, "blank json string")
56+
if err == nil {
57+
panic("err should not be nill")
58+
}
5659

5760
var null Int
5861
err = json.Unmarshal(nullJSON, &null)
@@ -68,8 +71,9 @@ func TestUnmarshalInt(t *testing.T) {
6871

6972
var invalid Int
7073
err = invalid.UnmarshalJSON(invalidJSON)
71-
if _, ok := err.(*json.SyntaxError); !ok {
72-
t.Errorf("expected json.SyntaxError, not %T", err)
74+
var syntaxError *json.SyntaxError
75+
if !errors.As(err, &syntaxError) {
76+
t.Errorf("expected wrapped json.SyntaxError, not %T", err)
7377
}
7478
assertNullInt(t, invalid, "invalid json")
7579
}
@@ -113,6 +117,12 @@ func TestTextUnmarshalInt(t *testing.T) {
113117
err = null.UnmarshalText([]byte("null"))
114118
maybePanic(err)
115119
assertNullInt(t, null, `UnmarshalText() "null"`)
120+
121+
var invalid Int
122+
err = invalid.UnmarshalText([]byte("hello world"))
123+
if err == nil {
124+
panic("expected error")
125+
}
116126
}
117127

118128
func TestMarshalInt(t *testing.T) {

0 commit comments

Comments
 (0)