Skip to content
This repository has been archived by the owner on Apr 21, 2023. It is now read-only.

Commit

Permalink
added flag for marshalling nil slices/maps to []/{} instead of null (#1)
Browse files Browse the repository at this point in the history
* added flag for marshalling nil slices to [] instead of null

* added similar functionality for nil maps

* fixed comment
  • Loading branch information
cbelsole authored Feb 21, 2018
1 parent 17dd88e commit 7b6e442
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 8 deletions.
27 changes: 27 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# json
This is a fork of [golang's encoding/json](https://github.com/golang/go/tree/master/src/encoding/json) with an extra method `MarshalSafeCollections` that marshals nil slices and maps into `[]` and `{}` respectfully instead of `null`. Additionally added `SetNilSafeCollection` to the `Encoder` in order to turn on the flag (default: false).

Currently master is a fork of `go1.9.4`.
34 changes: 30 additions & 4 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ func Marshal(v interface{}) ([]byte, error) {
return e.Bytes(), nil
}

// MarshalSafeCollections is like Marshal except it will marshal nil maps and
// slices as '{}' and '[]' respectfully instead of 'null'
func MarshalSafeCollections(v interface{}) ([]byte, error) {
e := &encodeState{}
err := e.marshal(v, encOpts{escapeHTML: true, nilSafeCollection: true})
if err != nil {
return nil, err
}
return e.Bytes(), nil
}

// MarshalIndent is like Marshal but applies Indent to format the output.
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
b, err := Marshal(v)
Expand Down Expand Up @@ -328,6 +339,9 @@ type encOpts struct {
quoted bool
// escapeHTML causes '<', '>', and '&' to be escaped in JSON strings.
escapeHTML bool
// nilSafeCollection marshals a nil slices and maps into '[]' and '{}'
// respectfully instead of 'null'
nilSafeCollection bool
}

type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
Expand Down Expand Up @@ -657,7 +671,11 @@ type mapEncoder struct {

func (me *mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
if v.IsNil() {
e.WriteString("null")
if opts.nilSafeCollection {
e.WriteString("{}")
} else {
e.WriteString("null")
}
return
}
e.WriteByte('{')
Expand Down Expand Up @@ -698,9 +716,13 @@ func newMapEncoder(t reflect.Type) encoderFunc {
return me.encode
}

func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
func encodeByteSlice(e *encodeState, v reflect.Value, opts encOpts) {
if v.IsNil() {
e.WriteString("null")
if opts.nilSafeCollection {
e.WriteString("[]")
} else {
e.WriteString("null")
}
return
}
s := v.Bytes()
Expand All @@ -727,7 +749,11 @@ type sliceEncoder struct {

func (se *sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
if v.IsNil() {
e.WriteString("null")
if opts.nilSafeCollection {
e.WriteString("[]")
} else {
e.WriteString("null")
}
return
}
se.arrayEnc(e, v, opts)
Expand Down
46 changes: 46 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -978,3 +978,49 @@ func TestMarshalRawMessageValue(t *testing.T) {
}
}
}

func TestMarshalSafeCollections(t *testing.T) {
var (
nilSlice []interface{}
pNilSlice *[]interface{}
nilMap map[string]interface{}
pNilMap *map[string]interface{}
)

type (
nilSliceStruct struct {
NilSlice []interface{} `json:"nil_slice"`
}
nilMapStruct struct {
NilMap map[string]interface{} `json:"nil_map"`
}
)

tests := []struct {
in interface{}
want string
}{
{nilSlice, "[]"},
{[]interface{}{}, "[]"},
{make([]interface{}, 0), "[]"},
{[]int{1, 2, 3}, "[1,2,3]"},
{pNilSlice, "null"},
{nilSliceStruct{}, "{\"nil_slice\":[]}"},
{nilMap, "{}"},
{map[string]interface{}{}, "{}"},
{make(map[string]interface{}, 0), "{}"},
{map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}"},
{pNilMap, "null"},
{nilMapStruct{}, "{\"nil_map\":{}}"},
}

for i, tt := range tests {
b, err := MarshalSafeCollections(tt.in)
if err != nil {
t.Errorf("test %d, unexpected failure: %v", i, err)
}
if got := string(b); got != tt.want {
t.Errorf("test %d, Marshal(%#v) = %q, want %q", i, tt.in, got, tt.want)
}
}
}
15 changes: 11 additions & 4 deletions stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ func nonSpace(b []byte) bool {

// An Encoder writes JSON values to an output stream.
type Encoder struct {
w io.Writer
err error
escapeHTML bool
w io.Writer
err error
escapeHTML bool
nilSafeCollection bool

indentBuf *bytes.Buffer
indentPrefix string
Expand All @@ -190,7 +191,7 @@ func (enc *Encoder) Encode(v interface{}) error {
return enc.err
}
e := newEncodeState()
err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML})
err := e.marshal(v, encOpts{escapeHTML: enc.escapeHTML, nilSafeCollection: enc.nilSafeCollection})
if err != nil {
return err
}
Expand Down Expand Up @@ -241,6 +242,12 @@ func (enc *Encoder) SetEscapeHTML(on bool) {
enc.escapeHTML = on
}

// SetNilSafeCollection specifies whether to represent nil slices and maps as
// '[]' or '{}' respectfully (flag on) instead of 'null' (default) when marshaling json.
func (enc *Encoder) SetNilSafeCollection(on bool) {
enc.nilSafeCollection = on
}

// RawMessage is a raw encoded JSON value.
// It implements Marshaler and Unmarshaler and can
// be used to delay JSON decoding or precompute a JSON encoding.
Expand Down
42 changes: 42 additions & 0 deletions stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,48 @@ func TestEncoderSetEscapeHTML(t *testing.T) {
}
}

func TestEncoderSetNilSafeCollection(t *testing.T) {
var (
nilSlice []interface{}
pNilSlice *[]interface{}
nilMap map[string]interface{}
pNilMap *map[string]interface{}
)
for _, tt := range []struct {
name string
v interface{}
want string
rescuedWant string
}{
{"nilSlice", nilSlice, "null", "[]"},
{"nonNilSlice", []interface{}{}, "[]", "[]"},
{"sliceWithValues", []interface{}{1, 2, 3}, "[1,2,3]", "[1,2,3]"},
{"pNilSlice", pNilSlice, "null", "null"},
{"nilMap", nilMap, "null", "{}"},
{"nonNilMap", map[string]interface{}{}, "{}", "{}"},
{"mapWithValues", map[string]interface{}{"1": 1, "2": 2, "3": 3}, "{\"1\":1,\"2\":2,\"3\":3}", "{\"1\":1,\"2\":2,\"3\":3}"},
{"pNilMap", pNilMap, "null", "null"},
} {
var buf bytes.Buffer
enc := NewEncoder(&buf)
if err := enc.Encode(tt.v); err != nil {
t.Fatalf("Encode(%s): %s", tt.name, err)
}
if got := strings.TrimSpace(buf.String()); got != tt.want {
t.Errorf("Encode(%s) = %#q, want %#q", tt.name, got, tt.want)
}
buf.Reset()
enc.SetNilSafeCollection(true)
if err := enc.Encode(tt.v); err != nil {
t.Fatalf("SetNilSafeCollection(true) Encode(%s): %s", tt.name, err)
}
if got := strings.TrimSpace(buf.String()); got != tt.rescuedWant {
t.Errorf("SetNilSafeCollection(true) Encode(%s) = %#q, want %#q",
tt.name, got, tt.want)
}
}
}

func TestDecoder(t *testing.T) {
for i := 0; i <= len(streamTest); i++ {
// Use stream without newlines as input,
Expand Down

0 comments on commit 7b6e442

Please sign in to comment.