├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bool.go ├── bool_test.go ├── doc.go ├── float64.go ├── float64_test.go ├── go.mod ├── int32.go ├── int32_test.go ├── int64.go ├── int64_test.go ├── nullgopher.svg ├── string.go ├── string_test.go ├── time.go └── time_test.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Report bugs on GitHub and discuss your ideas and feature requests before you open a pull request. Pull requests must pass the test suite and add new tests for each new feature. Bugs should be validated by tests. The Go code must be formatted by gofmt. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Emvi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nullable Go types 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/emvi/null?status.svg)](https://pkg.go.dev/github.com/emvi/null?status) 4 | [![CircleCI](https://circleci.com/gh/emvi/null.svg?style=svg)](https://circleci.com/gh/emvi/null) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/emvi/null)](https://goreportcard.com/report/github.com/emvi/null) 6 | Chat on Discord 7 | 8 | ## Description 9 | 10 | This package provides nullable Go types for bool, float64, int64, int32, string and time.Time replacing sql.NullString, sql.NullInt64, ... that can be marshalled/unmarshalled to/from JSON. 11 | 12 | ## Installation 13 | 14 | To install "null", run go get within your project: 15 | 16 | ``` 17 | go get github.com/emvi/null 18 | ``` 19 | 20 | Note that from 1.3 on "null" requires Go version 1.13 or newer. 21 | 22 | ## Usage 23 | 24 | Here is a short example demonstrating the string type. The other types (int64, float64 and bool) work in the same manner. 25 | 26 | ``` 27 | package main 28 | 29 | import ( 30 | "encoding/json" 31 | "database/sql" 32 | "fmt" 33 | 34 | "github.com/emvi/null" 35 | ) 36 | 37 | type NullableString struct { 38 | Value null.String `json:"value"` 39 | } 40 | 41 | func main() { 42 | str := NullableString{null.NewString("nullable string", true)} 43 | // or long version: str := NullableString{null.String{sql.NullString{String: "nullable string", Valid: true}}} 44 | 45 | data, _ := json.Marshal(str) 46 | fmt.Println(string(data)) // -> {"value": "nullable"} 47 | 48 | str.SetNil() // use str.SetValid("value") to set a value again 49 | data, _ = json.Marshal(str) 50 | fmt.Println(string(data)) // -> {"value": null} 51 | } 52 | ``` 53 | 54 | ## Contribute 55 | 56 | [See CONTRIBUTING.md](CONTRIBUTING.md) 57 | 58 | ## License 59 | 60 | MIT 61 | 62 |

63 | 64 |

65 | -------------------------------------------------------------------------------- /bool.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | // Bool is a nullable boolean type based on sql.NullBool, that supports parsing to/from JSON. 9 | type Bool struct { 10 | sql.NullBool 11 | } 12 | 13 | // NewBool returns a new nullable Bool object. 14 | // This is equivalent to `null.Bool{sql.NullBool{Bool: b, Valid: valid}}`. 15 | func NewBool(b, valid bool) Bool { 16 | return Bool{sql.NullBool{Bool: b, Valid: valid}} 17 | } 18 | 19 | // MarshalJSON implements the encoding json interface. 20 | func (b Bool) MarshalJSON() ([]byte, error) { 21 | if b.Valid { 22 | return json.Marshal(b.Bool) 23 | } 24 | 25 | return json.Marshal(nil) 26 | } 27 | 28 | // UnmarshalJSON implements the encoding json interface. 29 | func (b *Bool) UnmarshalJSON(data []byte) error { 30 | var value *bool 31 | 32 | if err := json.Unmarshal(data, &value); err != nil { 33 | return err 34 | } 35 | 36 | if value != nil { 37 | b.SetValid(*value) 38 | } else { 39 | b.SetNil() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // SetValid sets the value and valid to true. 46 | func (b *Bool) SetValid(value bool) { 47 | b.Bool = value 48 | b.Valid = true 49 | } 50 | 51 | // SetNil sets the value to default and valid to false. 52 | func (b *Bool) SetNil() { 53 | b.Bool = false 54 | b.Valid = false 55 | } 56 | -------------------------------------------------------------------------------- /bool_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testBool struct { 10 | Value Bool `json:"value"` 11 | } 12 | 13 | func TestNewBool(t *testing.T) { 14 | value := NewBool(true, true) 15 | 16 | if !value.Bool || !value.Valid { 17 | t.Fatal("New Bool must have value and be valid") 18 | } 19 | } 20 | 21 | func TestMarshalBool(t *testing.T) { 22 | value := Bool{sql.NullBool{Bool: true, Valid: true}} 23 | 24 | if data, err := json.Marshal(value); err != nil || string(data) != "true" { 25 | t.Fatalf("Bool must be marshalled to value, but was %v %v", err, string(data)) 26 | } 27 | 28 | value.Valid = false 29 | 30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 31 | t.Fatalf("Bool must be marshalled to null, but was %v %v", err, string(data)) 32 | } 33 | } 34 | 35 | func TestUnmarshalBool(t *testing.T) { 36 | str := `{"value": true}` 37 | var value testBool 38 | 39 | if err := json.Unmarshal([]byte(str), &value); err != nil { 40 | t.Fatalf("Bool must be unmarshalled to value, but was %v", err) 41 | } 42 | 43 | if !value.Value.Valid || !value.Value.Bool { 44 | t.Fatalf("Unmarshalled null bool must be valid, but was %v", value.Value) 45 | } 46 | 47 | str = `{"value": null}` 48 | 49 | if err := json.Unmarshal([]byte(str), &value); err != nil { 50 | t.Fatalf("Bool must be unmarshalled to null, but was %v", err) 51 | } 52 | 53 | if value.Value.Valid { 54 | t.Fatal("Unmarshalled null bool must be invalid") 55 | } 56 | } 57 | 58 | func TestGettersSettersBool(t *testing.T) { 59 | value := NewBool(true, true) 60 | value.SetNil() 61 | 62 | if value.Bool || value.Valid { 63 | t.Fatal("Bool must be nil") 64 | } 65 | 66 | value.SetValid(true) 67 | 68 | if !value.Bool || !value.Valid { 69 | t.Fatal("Bool must be valid") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package null provides nullable types (based on database/sql) that can be parsed to and from JSON. 2 | package null 3 | -------------------------------------------------------------------------------- /float64.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | // Float64 is a nullable float64 type based on sql.NullFloat64, that supports parsing to/from JSON. 9 | type Float64 struct { 10 | sql.NullFloat64 11 | } 12 | 13 | // NewFloat64 returns a new nullable Float64 object. 14 | // This is equivalent to `null.Float64{sql.NullFloat64{Float64: f, Valid: valid}}`. 15 | func NewFloat64(f float64, valid bool) Float64 { 16 | return Float64{sql.NullFloat64{Float64: f, Valid: valid}} 17 | } 18 | 19 | // MarshalJSON implements the encoding json interface. 20 | func (f Float64) MarshalJSON() ([]byte, error) { 21 | if f.Valid { 22 | return json.Marshal(f.Float64) 23 | } 24 | 25 | return json.Marshal(nil) 26 | } 27 | 28 | // UnmarshalJSON implements the encoding json interface. 29 | func (f *Float64) UnmarshalJSON(data []byte) error { 30 | var value *float64 31 | 32 | if err := json.Unmarshal(data, &value); err != nil { 33 | return err 34 | } 35 | 36 | if value != nil { 37 | f.SetValid(*value) 38 | } else { 39 | f.SetNil() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // SetValid sets the value and valid to true. 46 | func (f *Float64) SetValid(value float64) { 47 | f.Float64 = value 48 | f.Valid = true 49 | } 50 | 51 | // SetNil sets the value to default and valid to false. 52 | func (f *Float64) SetNil() { 53 | f.Float64 = 0 54 | f.Valid = false 55 | } 56 | -------------------------------------------------------------------------------- /float64_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testFloat64 struct { 10 | Value Float64 `json:"value"` 11 | } 12 | 13 | func TestNewFloat64(t *testing.T) { 14 | value := NewFloat64(123.45, true) 15 | 16 | if value.Float64 != 123.45 || !value.Valid { 17 | t.Fatal("New Float64 must have value and be valid") 18 | } 19 | } 20 | 21 | func TestMarshalFloat64(t *testing.T) { 22 | value := Float64{sql.NullFloat64{Float64: 123.45, Valid: true}} 23 | 24 | if data, err := json.Marshal(value); err != nil || string(data) != "123.45" { 25 | t.Fatalf("Float64 must be marshalled to value, but was %v %v", err, string(data)) 26 | } 27 | 28 | value.Valid = false 29 | 30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 31 | t.Fatalf("Float64 must be marshalled to null, but was %v %v", err, string(data)) 32 | } 33 | } 34 | 35 | func TestUnmarshalFloat64(t *testing.T) { 36 | str := `{"value": 123.45}` 37 | var value testFloat64 38 | 39 | if err := json.Unmarshal([]byte(str), &value); err != nil { 40 | t.Fatalf("Float64 must be unmarshalled to value, but was %v", err) 41 | } 42 | 43 | if !value.Value.Valid || value.Value.Float64 != 123.45 { 44 | t.Fatalf("Unmarshalled null float64 must be valid, but was %v", value.Value) 45 | } 46 | 47 | str = `{"value": null}` 48 | 49 | if err := json.Unmarshal([]byte(str), &value); err != nil { 50 | t.Fatalf("String must be unmarshalled to null, but was %v", err) 51 | } 52 | 53 | if value.Value.Valid { 54 | t.Fatal("Unmarshalled null float64 must be invalid") 55 | } 56 | } 57 | 58 | func TestGettersSettersFloat64(t *testing.T) { 59 | value := NewFloat64(123.45, true) 60 | value.SetNil() 61 | 62 | if value.Float64 != 0 || value.Valid { 63 | t.Fatal("Float64 must be nil") 64 | } 65 | 66 | value.SetValid(123.45) 67 | 68 | if value.Float64 != 123.45 || !value.Valid { 69 | t.Fatal("Float64 must be valid") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emvi/null 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /int32.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | // Int32 is a nullable int32 type based on sql.NullInt32, that supports parsing to/from JSON. 9 | type Int32 struct { 10 | sql.NullInt32 11 | } 12 | 13 | // NewInt32 returns a new nullable Int32 object. 14 | // This is equivalent to `null.Int32{sql.NullInt32{Int32: i, Valid: valid}}`. 15 | func NewInt32(i int32, valid bool) Int32 { 16 | return Int32{sql.NullInt32{Int32: i, Valid: valid}} 17 | } 18 | 19 | // MarshalJSON implements the encoding json interface. 20 | func (i Int32) MarshalJSON() ([]byte, error) { 21 | if i.Valid { 22 | return json.Marshal(i.Int32) 23 | } 24 | 25 | return json.Marshal(nil) 26 | } 27 | 28 | // UnmarshalJSON implements the encoding json interface. 29 | func (i *Int32) UnmarshalJSON(data []byte) error { 30 | var value *int32 31 | 32 | if err := json.Unmarshal(data, &value); err != nil { 33 | return err 34 | } 35 | 36 | if value != nil { 37 | i.SetValid(*value) 38 | } else { 39 | i.SetNil() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // SetValid sets the value and valid to true. 46 | func (i *Int32) SetValid(value int32) { 47 | i.Int32 = value 48 | i.Valid = true 49 | } 50 | 51 | // SetNil sets the value to default and valid to false. 52 | func (i *Int32) SetNil() { 53 | i.Int32 = 0 54 | i.Valid = false 55 | } 56 | -------------------------------------------------------------------------------- /int32_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testInt32 struct { 10 | Value Int32 `json:"value"` 11 | } 12 | 13 | func TestNewInt32(t *testing.T) { 14 | value := NewInt32(123, true) 15 | 16 | if value.Int32 != 123 || !value.Valid { 17 | t.Fatal("New Int32 must have value and be valid") 18 | } 19 | } 20 | 21 | func TestMarshalInt32(t *testing.T) { 22 | value := Int32{sql.NullInt32{Int32: 123, Valid: true}} 23 | 24 | if data, err := json.Marshal(value); err != nil || string(data) != "123" { 25 | t.Fatalf("Int32 must be marshalled to value, but was %v %v", err, string(data)) 26 | } 27 | 28 | value.Valid = false 29 | 30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 31 | t.Fatalf("Int32 must be marshalled to null, but was %v %v", err, string(data)) 32 | } 33 | } 34 | 35 | func TestUnmarshalInt32(t *testing.T) { 36 | str := `{"value": 123}` 37 | var value testInt32 38 | 39 | if err := json.Unmarshal([]byte(str), &value); err != nil { 40 | t.Fatalf("Int32 must be unmarshalled to value, but was %v", err) 41 | } 42 | 43 | if !value.Value.Valid || value.Value.Int32 != 123 { 44 | t.Fatalf("Unmarshalled null Int32 must be valid, but was %v", value.Value) 45 | } 46 | 47 | str = `{"value": null}` 48 | 49 | if err := json.Unmarshal([]byte(str), &value); err != nil { 50 | t.Fatalf("Int32 must be unmarshalled to null, but was %v", err) 51 | } 52 | 53 | if value.Value.Valid { 54 | t.Fatal("Unmarshalled null Int32 must be invalid") 55 | } 56 | } 57 | 58 | func TestGettersSettersInt32(t *testing.T) { 59 | value := NewInt32(123, true) 60 | value.SetNil() 61 | 62 | if value.Int32 != 0 || value.Valid { 63 | t.Fatal("Int32 must be nil") 64 | } 65 | 66 | value.SetValid(123) 67 | 68 | if value.Int32 != 123 || !value.Valid { 69 | t.Fatal("Int32 must be valid") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /int64.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | // Int64 is a nullable int64 type based on sql.NullInt64, that supports parsing to/from JSON. 9 | type Int64 struct { 10 | sql.NullInt64 11 | } 12 | 13 | // NewInt64 returns a new nullable Int64 object. 14 | // This is equivalent to `null.Int64{sql.NullInt64{Int64: i, Valid: valid}}`. 15 | func NewInt64(i int64, valid bool) Int64 { 16 | return Int64{sql.NullInt64{Int64: i, Valid: valid}} 17 | } 18 | 19 | // MarshalJSON implements the encoding json interface. 20 | func (i Int64) MarshalJSON() ([]byte, error) { 21 | if i.Valid { 22 | return json.Marshal(i.Int64) 23 | } 24 | 25 | return json.Marshal(nil) 26 | } 27 | 28 | // UnmarshalJSON implements the encoding json interface. 29 | func (i *Int64) UnmarshalJSON(data []byte) error { 30 | var value *int64 31 | 32 | if err := json.Unmarshal(data, &value); err != nil { 33 | return err 34 | } 35 | 36 | if value != nil { 37 | i.SetValid(*value) 38 | } else { 39 | i.SetNil() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // SetValid sets the value and valid to true. 46 | func (i *Int64) SetValid(value int64) { 47 | i.Int64 = value 48 | i.Valid = true 49 | } 50 | 51 | // SetNil sets the value to default and valid to false. 52 | func (i *Int64) SetNil() { 53 | i.Int64 = 0 54 | i.Valid = false 55 | } 56 | -------------------------------------------------------------------------------- /int64_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testInt64 struct { 10 | Value Int64 `json:"value"` 11 | } 12 | 13 | func TestNewInt64(t *testing.T) { 14 | value := NewInt64(123, true) 15 | 16 | if value.Int64 != 123 || !value.Valid { 17 | t.Fatal("New Int64 must have value and be valid") 18 | } 19 | } 20 | 21 | func TestMarshalInt64(t *testing.T) { 22 | value := Int64{sql.NullInt64{Int64: 123, Valid: true}} 23 | 24 | if data, err := json.Marshal(value); err != nil || string(data) != "123" { 25 | t.Fatalf("Int64 must be marshalled to value, but was %v %v", err, string(data)) 26 | } 27 | 28 | value.Valid = false 29 | 30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 31 | t.Fatalf("Int64 must be marshalled to null, but was %v %v", err, string(data)) 32 | } 33 | } 34 | 35 | func TestUnmarshalInt64(t *testing.T) { 36 | str := `{"value": 123}` 37 | var value testInt64 38 | 39 | if err := json.Unmarshal([]byte(str), &value); err != nil { 40 | t.Fatalf("Int64 must be unmarshalled to value, but was %v", err) 41 | } 42 | 43 | if !value.Value.Valid || value.Value.Int64 != 123 { 44 | t.Fatalf("Unmarshalled null int64 must be valid, but was %v", value.Value) 45 | } 46 | 47 | str = `{"value": null}` 48 | 49 | if err := json.Unmarshal([]byte(str), &value); err != nil { 50 | t.Fatalf("Int64 must be unmarshalled to null, but was %v", err) 51 | } 52 | 53 | if value.Value.Valid { 54 | t.Fatal("Unmarshalled null int64 must be invalid") 55 | } 56 | } 57 | 58 | func TestGettersSettersInt64(t *testing.T) { 59 | value := NewInt64(123, true) 60 | value.SetNil() 61 | 62 | if value.Int64 != 0 || value.Valid { 63 | t.Fatal("Int64 must be nil") 64 | } 65 | 66 | value.SetValid(123) 67 | 68 | if value.Int64 != 123 || !value.Valid { 69 | t.Fatal("Int64 must be valid") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /nullgopher.svg: -------------------------------------------------------------------------------- 1 | Element 1 -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | ) 7 | 8 | // String is a nullable string type based on sql.NullString, that supports parsing to/from JSON. 9 | type String struct { 10 | sql.NullString 11 | } 12 | 13 | // NewString returns a new nullable String object. 14 | // This is equivalent to `null.String{sql.NullString{String: s, Valid: valid}}`. 15 | func NewString(s string, valid bool) String { 16 | return String{sql.NullString{String: s, Valid: valid}} 17 | } 18 | 19 | // MarshalJSON implements the encoding json interface. 20 | func (s String) MarshalJSON() ([]byte, error) { 21 | if s.Valid { 22 | return json.Marshal(s.String) 23 | } 24 | 25 | return json.Marshal(nil) 26 | } 27 | 28 | // UnmarshalJSON implements the encoding json interface. 29 | func (s *String) UnmarshalJSON(data []byte) error { 30 | var value *string 31 | 32 | if err := json.Unmarshal(data, &value); err != nil { 33 | return err 34 | } 35 | 36 | if value != nil { 37 | s.SetValid(*value) 38 | } else { 39 | s.SetNil() 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // SetValid sets the value and valid to true. 46 | func (s *String) SetValid(value string) { 47 | s.String = value 48 | s.Valid = true 49 | } 50 | 51 | // SetNil sets the value to default and valid to false. 52 | func (s *String) SetNil() { 53 | s.String = "" 54 | s.Valid = false 55 | } 56 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | type testString struct { 10 | Value String `json:"value"` 11 | } 12 | 13 | func TestNewString(t *testing.T) { 14 | value := NewString("test", true) 15 | 16 | if value.String != "test" || !value.Valid { 17 | t.Fatal("New String must have value and be valid") 18 | } 19 | } 20 | 21 | func TestMarshalString(t *testing.T) { 22 | value := String{sql.NullString{String: "test", Valid: true}} 23 | 24 | if data, err := json.Marshal(value); err != nil || string(data) != "\"test\"" { 25 | t.Fatalf("String must be marshalled to value, but was %v %v", err, string(data)) 26 | } 27 | 28 | value.Valid = false 29 | 30 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 31 | t.Fatalf("String must be marshalled to null, but was %v %v", err, string(data)) 32 | } 33 | } 34 | 35 | func TestUnmarshalString(t *testing.T) { 36 | str := `{"value": "test"}` 37 | var value testString 38 | 39 | if err := json.Unmarshal([]byte(str), &value); err != nil { 40 | t.Fatalf("String must be unmarshalled to value, but was %v", err) 41 | } 42 | 43 | if !value.Value.Valid || value.Value.String != "test" { 44 | t.Fatalf("Unmarshalled null string must be valid, but was %v", value.Value) 45 | } 46 | 47 | str = `{"value": null}` 48 | 49 | if err := json.Unmarshal([]byte(str), &value); err != nil { 50 | t.Fatalf("String must be unmarshalled to null, but was %v", err) 51 | } 52 | 53 | if value.Value.Valid { 54 | t.Fatal("Unmarshalled null string must be invalid") 55 | } 56 | } 57 | 58 | func TestGettersSettersString(t *testing.T) { 59 | value := NewString("test", true) 60 | value.SetNil() 61 | 62 | if value.String != "" || value.Valid { 63 | t.Fatal("String must be nil") 64 | } 65 | 66 | value.SetValid("test") 67 | 68 | if value.String != "test" || !value.Valid { 69 | t.Fatal("String must be valid") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "errors" 8 | "time" 9 | ) 10 | 11 | // Time is a nullable time.Time, that supports parsing to/from JSON. 12 | type Time struct { 13 | sql.NullTime 14 | } 15 | 16 | // NewTime returns a new nullable time.Time object. 17 | // This is equivalent to `null.Time{Time: t, Valid: valid}`. 18 | func NewTime(t time.Time, valid bool) Time { 19 | return Time{sql.NullTime{Time: t, Valid: valid}} 20 | } 21 | 22 | // MarshalJSON implements the encoding json interface. 23 | func (t Time) MarshalJSON() ([]byte, error) { 24 | if t.Valid { 25 | return json.Marshal(t.Time) 26 | } 27 | 28 | return json.Marshal(nil) 29 | } 30 | 31 | // UnmarshalJSON implements the encoding json interface. 32 | func (t *Time) UnmarshalJSON(data []byte) error { 33 | var value time.Time 34 | 35 | if err := json.Unmarshal(data, &value); err != nil { 36 | return err 37 | } 38 | 39 | if !value.IsZero() { 40 | t.SetValid(value) 41 | } else { 42 | t.SetNil() 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // Scan implements the Scanner interface. 49 | func (t *Time) Scan(value interface{}) error { 50 | if value == nil { 51 | t.SetNil() 52 | return nil 53 | } 54 | 55 | v, ok := value.(time.Time) 56 | 57 | if !ok { 58 | return errors.New("unexpected type") 59 | } 60 | 61 | t.SetValid(v) 62 | return nil 63 | } 64 | 65 | // Value implements the driver Valuer interface. 66 | func (t Time) Value() (driver.Value, error) { 67 | if !t.Valid { 68 | return nil, nil 69 | } 70 | 71 | return t.Time, nil 72 | } 73 | 74 | // SetValid sets the value and valid to true. 75 | func (t *Time) SetValid(value time.Time) { 76 | t.Time = value 77 | t.Valid = true 78 | } 79 | 80 | // SetNil sets the value to default and valid to false. 81 | func (t *Time) SetNil() { 82 | t.Time = time.Time{} 83 | t.Valid = false 84 | } 85 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package null 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type testTime struct { 11 | Value Time `json:"value"` 12 | } 13 | 14 | func TestNewTime(t *testing.T) { 15 | value := NewTime(time.Now(), true) 16 | 17 | if value.Time.Equal(time.Time{}) || !value.Valid { 18 | t.Fatal("New Time must have value and be valid") 19 | } 20 | } 21 | 22 | func TestMarshalTime(t *testing.T) { 23 | now := time.Now() 24 | nowStrBytes, _ := json.Marshal(now) 25 | nowStr := string(nowStrBytes) 26 | value := Time{sql.NullTime{Time: now, Valid: true}} 27 | 28 | if data, err := json.Marshal(value); err != nil || string(data) != nowStr { 29 | t.Fatalf("Time must be marshalled to value, but was %v %v", err, string(data)) 30 | } 31 | 32 | value.Valid = false 33 | 34 | if data, err := json.Marshal(value); err != nil || string(data) != "null" { 35 | t.Fatalf("Time must be marshalled to null, but was %v %v", err, string(data)) 36 | } 37 | } 38 | 39 | func TestUnmarshalTime(t *testing.T) { 40 | now := time.Now() 41 | nowStrBytes, _ := json.Marshal(now) 42 | nowStr := string(nowStrBytes) 43 | str := `{"value": ` + nowStr + `}` 44 | var value testTime 45 | 46 | if err := json.Unmarshal([]byte(str), &value); err != nil { 47 | t.Fatalf("Time must be unmarshalled to value, but was %v", err) 48 | } 49 | 50 | if !value.Value.Valid || !value.Value.Time.Equal(now) { 51 | t.Fatalf("Unmarshalled null Time must be valid, but was %v", value.Value) 52 | } 53 | 54 | str = `{"value": null}` 55 | 56 | if err := json.Unmarshal([]byte(str), &value); err != nil { 57 | t.Fatalf("Time must be unmarshalled to null, but was %v", err) 58 | } 59 | 60 | if value.Value.Valid { 61 | t.Fatal("Unmarshalled null Time must be invalid") 62 | } 63 | } 64 | 65 | func TestScanTime(t *testing.T) { 66 | now := time.Now() 67 | str := "test" 68 | var value Time 69 | 70 | if err := value.Scan(&str); err == nil || err.Error() != "unexpected type" { 71 | t.Fatalf("Time must return error, but was: %v", err) 72 | } 73 | 74 | if err := value.Scan(now); err != nil { 75 | t.Fatalf("Time must be scanned, but was: %v", err) 76 | } 77 | 78 | if !value.Time.Equal(now) { 79 | t.Fatalf("Scanned time must be equal to input, but was: %v == %v", value.Time, now) 80 | } 81 | } 82 | 83 | func TestValueTime(t *testing.T) { 84 | value := NewTime(time.Now(), true) 85 | out, err := value.Value() 86 | 87 | if err != nil || out == nil { 88 | t.Fatalf("Time must return value, but was: %v", err) 89 | } 90 | } 91 | 92 | func TestGettersSettersTime(t *testing.T) { 93 | value := NewTime(time.Now(), true) 94 | value.SetNil() 95 | 96 | if !value.Time.Equal(time.Time{}) || value.Valid { 97 | t.Fatal("Time must be nil") 98 | } 99 | 100 | value.SetValid(time.Now()) 101 | 102 | if value.Time.Equal(time.Time{}) || !value.Valid { 103 | t.Fatal("Time must be valid") 104 | } 105 | } 106 | --------------------------------------------------------------------------------