diff --git a/README.md b/README.md index 1cd4bd5..6839f3a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: 2024 Winni Neessen SPDX-License-Identifier: CC0-1.0 --> -# niljson - A simple Go package for unmarshalling null-able JSON types +# niljson - A simple Go package for (un-)marshalling null-able JSON types [![GoDoc](https://godoc.org/github.com/wneessen/niljson?status.svg)](https://pkg.go.dev/github.com/wneessen/niljson) [![codecov](https://codecov.io/gh/wneessen/niljson/branch/main/graph/badge.svg?token=W4QI1RMR4L)](https://codecov.io/gh/wneessen/niljson) @@ -12,7 +12,7 @@ SPDX-License-Identifier: CC0-1.0 [![REUSE status](https://api.reuse.software/badge/github.com/wneessen/niljson)](https://api.reuse.software/info/github.com/wneessen/niljson) buy ma a coffee -niljson provides a simple and efficient way to handle nullable JSON fields during the unmarshalling process. +niljson provides a simple and efficient way to handle nullable JSON fields during the (un-)marshalling process. In JSON, it's common to encounter fields that can be `null`, but handling these fields in Go can be cumbersome, especially when dealing with primitive types like `int`, `float64`, `bool`. These types can all be either `0` (as value) or `null`. In Go you can always work with pointers but these, of course, can lead to unhandled nil pointer dereferences. @@ -25,9 +25,7 @@ checks for `nil` values. - **Nullable Types**: Provides a range of nullable types (`NilString`, `NilInt`, `NilFloat`, `NilBool`, etc.) that are easy to use and integrate into your existing Go structs. -- **Seamless Integration**: These types work just like Go's standard types but add support for `null` values, - enabling cleaner and more maintainable code. -- **JSON Unmarshalling Support**: Automatically handles the unmarshalling of JSON fields, converting `null` JSON +- **JSON Unmarshalling Support**: Automatically handles the (un-)marshalling of JSON fields, converting `null` JSON values to Go's `nil` or zero values, depending on the context. - **Minimalistic and Lightweight**: Designed to be lightweight and unobtrusive, so it won't bloat your application or introduce unnecessary dependencies (only relies on the Go standard library) @@ -87,5 +85,12 @@ func main() { output += fmt.Sprintf("String is: %s", example.String.Value()) } fmt.Println(output) + + data, err := json.Marshal(&example) + if err != nil { + fmt.Printf("failed to marshal JSON: %s", err) + os.Exit(1) + } + fmt.Println(data) } ``` diff --git a/niljson.go b/niljson.go index 2f01999..95312ea 100644 --- a/niljson.go +++ b/niljson.go @@ -36,6 +36,14 @@ func (v *Variable[T]) Reset() { v.notNil = false } +// NewVariable returns a new Variable of generic type +func NewVariable[T any](value T) Variable[T] { + return Variable[T]{ + notNil: true, + value: value, + } +} + // NilBoolean is an boolean type that can be nil type NilBoolean = Variable[bool] @@ -72,7 +80,7 @@ type NilFloat64 = Variable[float64] // NilString is a string type that can be nil type NilString = Variable[string] -// UnmarshalJSON interprets the generic Nil types and sets the value and notnil of the type +// UnmarshalJSON satisfies the json.Unmarshaler interface for generic Variable types func (v *Variable[T]) UnmarshalJSON(data []byte) error { if string(data) != "null" { v.value = *new(T) @@ -81,3 +89,11 @@ func (v *Variable[T]) UnmarshalJSON(data []byte) error { } return nil } + +// MarshalJSON satisfies the json.Marshaler interface for generic Variable types +func (v *Variable[T]) MarshalJSON() ([]byte, error) { + if !v.notNil { + return json.Marshal(nil) + } + return json.Marshal(v.value) +} diff --git a/niljson_test.go b/niljson_test.go index 458dd34..315029c 100644 --- a/niljson_test.go +++ b/niljson_test.go @@ -56,6 +56,25 @@ func TestVariable_UnmarshalJSON_Boolean(t *testing.T) { } } +func TestVariable_MarshalJSON_Boolean(t *testing.T) { + type JSONType struct { + Value NilBoolean `json:"bool"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"bool":false,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(false), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_ByteSlice(t *testing.T) { type JSONType struct { Value NilByteSlice `json:"bytes"` @@ -83,6 +102,25 @@ func TestVariable_UnmarshalJSON_ByteSlice(t *testing.T) { } } +func TestVariable_MarshalJSON_ByteSlice(t *testing.T) { + type JSONType struct { + Value NilByteSlice `json:"bytes"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"bytes":"Ynl0ZXM=","nilvalue":null}` + jt := &JSONType{ + Value: NewVariable([]byte("bytes")), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_Float32(t *testing.T) { type JSONType struct { Value NilFloat32 `json:"float32"` @@ -111,6 +149,25 @@ func TestVariable_UnmarshalJSON_Float32(t *testing.T) { } } +func TestVariable_MarshalJSON_Float32(t *testing.T) { + type JSONType struct { + Value NilFloat32 `json:"float32"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"float32":1.234,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(float32(1.234)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_Float64(t *testing.T) { type JSONType struct { Value NilFloat64 `json:"float64"` @@ -139,6 +196,25 @@ func TestVariable_UnmarshalJSON_Float64(t *testing.T) { } } +func TestVariable_MarshalJSON_Float64(t *testing.T) { + type JSONType struct { + Value NilFloat64 `json:"float64"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"float64":123.456,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(123.456), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_Int(t *testing.T) { type JSONType struct { Value NilInt `json:"int"` @@ -167,6 +243,25 @@ func TestVariable_UnmarshalJSON_Int(t *testing.T) { } } +func TestVariable_MarshalJSON_Int(t *testing.T) { + type JSONType struct { + Value NilInt `json:"int"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"int":123,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(123), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_Int64(t *testing.T) { type JSONType struct { Value NilInt64 `json:"int64"` @@ -195,6 +290,25 @@ func TestVariable_UnmarshalJSON_Int64(t *testing.T) { } } +func TestVariable_MarshalJSON_Int64(t *testing.T) { + type JSONType struct { + Value NilInt64 `json:"int64"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"int64":12345678901234,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(int64(12345678901234)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_String(t *testing.T) { type JSONType struct { Value NilString `json:"string"` @@ -223,6 +337,25 @@ func TestVariable_UnmarshalJSON_String(t *testing.T) { } } +func TestVariable_MarshalJSON_String(t *testing.T) { + type JSONType struct { + Value NilString `json:"string"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"string":"test123","nilvalue":null}` + jt := &JSONType{ + Value: NewVariable("test123"), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_UInt(t *testing.T) { type JSONType struct { Value NilUInt `json:"uint"` @@ -251,6 +384,25 @@ func TestVariable_UnmarshalJSON_UInt(t *testing.T) { } } +func TestVariable_MarshalJSON_UInt(t *testing.T) { + type JSONType struct { + Value NilUInt `json:"uint"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"uint":123,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(uint(123)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_UInt8(t *testing.T) { type JSONType struct { Value NilUInt8 `json:"uint8"` @@ -279,6 +431,25 @@ func TestVariable_UnmarshalJSON_UInt8(t *testing.T) { } } +func TestVariable_MarshalJSON_UInt8(t *testing.T) { + type JSONType struct { + Value NilUInt8 `json:"uint8"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"uint8":1,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(uint8(1)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_UInt16(t *testing.T) { type JSONType struct { Value NilUInt16 `json:"uint16"` @@ -307,6 +478,25 @@ func TestVariable_UnmarshalJSON_UInt16(t *testing.T) { } } +func TestVariable_MarshalJSON_UInt16(t *testing.T) { + type JSONType struct { + Value NilUInt16 `json:"uint16"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"uint16":2,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(uint16(2)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_UInt32(t *testing.T) { type JSONType struct { Value NilUInt32 `json:"uint32"` @@ -335,6 +525,25 @@ func TestVariable_UnmarshalJSON_UInt32(t *testing.T) { } } +func TestVariable_MarshalJSON_UInt32(t *testing.T) { + type JSONType struct { + Value NilUInt32 `json:"uint32"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"uint32":3,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(uint32(3)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func TestVariable_UnmarshalJSON_UInt64(t *testing.T) { type JSONType struct { Value NilUInt64 `json:"uint64"` @@ -363,6 +572,25 @@ func TestVariable_UnmarshalJSON_UInt64(t *testing.T) { } } +func TestVariable_MarshalJSON_UInt64(t *testing.T) { + type JSONType struct { + Value NilUInt64 `json:"uint64"` + NilValue NilBoolean `json:"nilvalue,omitempty"` + } + + expected := `{"uint64":4,"nilvalue":null}` + jt := &JSONType{ + Value: NewVariable(uint64(4)), + } + data, err := json.Marshal(&jt) + if err != nil { + t.Errorf("failed to marshal json with nil types: %s", err) + } + if !bytes.Equal(data, []byte(expected)) { + t.Errorf("expected json to be %q, got %q", expected, string(data)) + } +} + func ExampleVariable_UnmarshalJSON() { type JSONType struct { Bool NilBoolean `json:"bool"` @@ -407,3 +635,35 @@ func ExampleVariable_UnmarshalJSON() { fmt.Println(output) // Output: Bool is: true, Float 32 is nil, Float 64 is: 0.000000, String is: test } + +func ExampleVariable_MarshalJSON() { + type JSONType struct { + Bool NilBoolean `json:"bool"` + ByteSlice NilByteSlice `json:"bytes"` + Float32 NilFloat32 `json:"float32,omitempty"` + Float64 NilFloat64 `json:"float64"` + Int NilInt `json:"int"` + Int64 NilInt64 `json:"int64"` + NullString NilString `json:"nilvalue,omitempty"` + String NilString `json:"string"` + UInt NilUInt `json:"uint"` + UInt8 NilUInt8 `json:"uint8"` + } + + example := &JSONType{ + Bool: NewVariable(false), + ByteSlice: NewVariable([]byte("bytes")), + Float64: NewVariable(123.456), + Int: NewVariable(123), + Int64: NewVariable(int64(12345678901234)), + String: NewVariable("test"), + UInt: NewVariable(uint(123)), + } + data, err := json.Marshal(example) + if err != nil { + fmt.Printf("failed to marshal JSON: %s", err) + os.Exit(1) + } + fmt.Println(string(data)) + // Output: {"bool":false,"bytes":"Ynl0ZXM=","float32":null,"float64":123.456,"int":123,"int64":12345678901234,"nilvalue":null,"string":"test","uint":123,"uint8":null} +}