Skip to content

Commit

Permalink
Merge pull request #13 from moznion/json_serde
Browse files Browse the repository at this point in the history
Support JSON marshal and unmarshal for Option[T] value
  • Loading branch information
moznion committed Sep 5, 2022
2 parents fe94ce0 + 6bd9177 commit ebd44c5
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 0 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,62 @@ and more detailed examples are here: [./examples_test.go](./examples_test.go).
- [Option.Unzip[T, U any](zipped Option[Pair[T, U]]) (Option[T], Option[U])](https://pkg.go.dev/github.com/moznion/go-optional#Unzip)
- [Option.UnzipWith[T, U, V any](zipped Option[V], unzipper func(zipped V) (T, U)) (Option[T], Option[U])](https://pkg.go.dev/github.com/moznion/go-optional#UnzipWith)

### JSON marshal/unmarshal support

This `Option[T]` type supports JSON marshal and unmarshal.

If the value wanted to marshal is `Some[T]` then it marshals that value into the JSON bytes simply, and in unmarshaling, if the given JSON string/bytes has the actual value on corresponded property, it unmarshals that value into `Some[T]` value.

example:

```go
type JSONStruct struct {
Val Option[int] `json:"val"`
}

some := Some[int](123)
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
if err != nil {
return err
}
fmt.Printf("%s\n", marshal) // => {"val":123}

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
if err != nil {
return err
}
// unmarshalJSONStruct.Val == Some[int](123)
```

Elsewise, when the value is `None[T]`, the marshaller serializes that value as `null`. And if the unmarshaller gets the JSON `null` value on a property corresponding to the `Optional[T]` value, or the value of a property is missing, that deserializes that value as `None[T]`.

example:

```go
type JSONStruct struct {
Val Option[int] `json:"val"`
}

some := None[int]()
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
if err != nil {
return err
}
fmt.Printf("%s\n", marshal) // => {"val":null}

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
if err != nil {
return err
}
// unmarshalJSONStruct.Val == None[int]()
```

## Tips

- it would be better to deal with an Option value as a non-pointer because if the Option value can accept nil it becomes worthless
Expand Down
32 changes: 32 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package optional

import (
"bytes"
"encoding/json"
"errors"
)

Expand Down Expand Up @@ -270,3 +272,33 @@ func UnzipWith[T, U, V any](zipped Option[V], unzipper func(zipped V) (T, U)) (O
v1, v2 := unzipper(zipped.value)
return Some(v1), Some(v2)
}

var jsonNull = []byte("null")

func (o Option[T]) MarshalJSON() ([]byte, error) {
if o.IsNone() {
return jsonNull, nil
}

marshal, err := json.Marshal(o.Unwrap())
if err != nil {
return nil, err
}
return marshal, nil
}

func (o *Option[T]) UnmarshalJSON(data []byte) error {
if len(data) <= 0 || bytes.Equal(data, jsonNull) {
*o = None[T]()
return nil
}

var v T
err := json.Unmarshal(data, &v)
if err != nil {
return err
}
*o = Some(v)

return nil
}
151 changes: 151 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package optional

import (
"encoding/json"
"errors"
"fmt"
"testing"
Expand Down Expand Up @@ -369,3 +370,153 @@ func TestFlatMapOrWithError(t *testing.T) {
assert.ErrorIs(t, err, mapperError)
assert.Equal(t, "", mapped)
}

func TestOptionSerdeJSONForSomeValue(t *testing.T) {
{
type JSONStruct struct {
Val Option[int] `json:"val"`
}

some := Some[int](123)
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":123}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}

{
type JSONStruct struct {
Val Option[string] `json:"val"`
}

some := Some[string]("foobar")
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":"foobar"}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}

{
type JSONStruct struct {
Val Option[bool] `json:"val"`
}

some := Some[bool](false)
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":false}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}

{
type Inner struct {
B *bool `json:"b,omitempty"`
}
type JSONStruct struct {
Val Option[Inner] `json:"val"`
}

{
falsy := false
some := Some[Inner](Inner{
B: &falsy,
})
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":{"b":false}}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}

{
some := Some[Inner](Inner{
B: nil,
})
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":{}}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}
}
}

func TestOptionSerdeJSONForNoneValue(t *testing.T) {
type JSONStruct struct {
Val Option[int] `json:"val"`
}
some := None[int]()
jsonStruct := &JSONStruct{Val: some}

marshal, err := json.Marshal(jsonStruct)
assert.NoError(t, err)
assert.EqualValues(t, string(marshal), `{"val":null}`)

var unmarshalJSONStruct JSONStruct
err = json.Unmarshal(marshal, &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, jsonStruct, &unmarshalJSONStruct)
}

func TestOption_UnmarshalJSON_withEmptyJSONString(t *testing.T) {
type JSONStruct struct {
Val Option[int] `json:"val"`
}

var unmarshalJSONStruct JSONStruct
err := json.Unmarshal([]byte("{}"), &unmarshalJSONStruct)
assert.NoError(t, err)
assert.EqualValues(t, &JSONStruct{
Val: None[int](),
}, &unmarshalJSONStruct)
}

func TestOption_MarshalJSON_shouldReturnErrorWhenInvalidJSONStructInputHasCome(t *testing.T) {
type JSONStruct struct {
Val Option[chan interface{}] `json:"val"` // chan type is unsupported on json marshaling
}

ch := make(chan interface{})
some := Some[chan interface{}](ch)
jsonStruct := &JSONStruct{Val: some}
_, err := json.Marshal(jsonStruct)
assert.Error(t, err)
}

func TestOption_UnmarshalJSON_shouldReturnErrorWhenInvalidJSONStringInputHasCome(t *testing.T) {
type JSONStruct struct {
Val Option[int] `json:"val"`
}

var unmarshalJSONStruct JSONStruct
err := json.Unmarshal([]byte(`{"val":"__STRING__"}`), &unmarshalJSONStruct)
assert.Error(t, err)
}

0 comments on commit ebd44c5

Please sign in to comment.