From 05d806adffd6e3077f7cce23397a37924983d40b Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 2 Sep 2024 12:13:54 +0200 Subject: [PATCH 1/2] Add JSON marshaling support for Variable types Introduced a NewVariable function for creating generics-based Variable types. Added MarshalJSON methods to support JSON encoding for various Nil types, ensuring proper handling of nil values. Updated tests and examples to verify the new marshaling functionality. --- README.md | 7 ++ niljson.go | 18 +++- niljson_test.go | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cd4bd5..04480a7 100644 --- a/README.md +++ b/README.md @@ -87,5 +87,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} +} From dee653ae88744a231768bbb85f8a9d74eabc3542 Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Mon, 2 Sep 2024 12:15:38 +0200 Subject: [PATCH 2/2] Update README to include marshalling capability Expanded the description and features to cover both marshalling and unmarshalling of JSON fields. This clarifies the package's functionality, making it clear that it handles not only null values during unmarshalling but also during marshalling. --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 04480a7..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)