Skip to content

Commit

Permalink
feat: omitnil tag support for json.Marshaler fields
Browse files Browse the repository at this point in the history
The struct fields that implement the MarshalJSON method of the
json.Marshaler interface can now use the omitnil tag to omit their
output if it matches the JSON 'null' value.

Close #5
  • Loading branch information
wI2L committed Jan 6, 2023
1 parent c12aee0 commit 45899e5
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ All notable differences with the standard library behavior are listed below. Ple

- The `sync.Map` type is handled natively. The marshaling behavior is similar to the one of a standard Go `map`. The option `UnsortedMap` can also be used in cunjunction with this type to disable the default keys sort.

- The `omitnil` field tag's option can be used to specify that a field with a nil pointer should be omitted from the encoding. This option has precedence over the `omitempty` option.
- The `omitnil` field tag's option can be used to specify that a field with a nil pointer should be omitted from the encoding. This option has precedence over the `omitempty` option. Note that struct fields that implement the `json.Marshaler` interface will be omitted too, if they return the literal JSON `null` value.

#### Bugs

Expand Down
20 changes: 14 additions & 6 deletions encode.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jettison

import (
"bytes"
"encoding"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -203,16 +204,16 @@ fieldLoop:
if opts.isDeniedField(f.name) {
continue
}
v := p
fp := p

// Find the nested struct field by following
// the offset sequence, indirecting encountered
// pointers as needed.
for i := 0; i < len(f.embedSeq); i++ {
s := &f.embedSeq[i]
v = unsafe.Pointer(uintptr(v) + s.offset)
fp = unsafe.Pointer(uintptr(fp) + s.offset)
if s.indir {
if v = *(*unsafe.Pointer)(v); v == nil {
if fp = *(*unsafe.Pointer)(fp); fp == nil {
// When we encounter a nil pointer
// in the chain, we have no choice
// but to ignore the field.
Expand All @@ -222,28 +223,35 @@ fieldLoop:
}
// Ignore the field if it is a nil pointer and has
// the omitnil option in his tag.
if f.omitNil && *(*unsafe.Pointer)(v) == nil {
if f.omitNil && *(*unsafe.Pointer)(fp) == nil {
continue
}
// Ignore the field if it represents the zero-value
// of its type and has the omitempty option in his tag.
// Empty func is non-nil only if the field has the
// omitempty option in its tag.
if f.omitEmpty && f.empty(v) {
if f.omitEmpty && f.empty(fp) {
continue
}
key = f.keyEscHTML
if noHTMLEscape {
key = f.keyNonEsc
}
lastKeyOffset := len(dst)
dst = append(dst, nxt)
if nxt == '{' {
lastKeyOffset++
}
nxt = ','
dst = append(dst, key...)

var err error
if dst, err = f.instr(v, dst, opts); err != nil {
if dst, err = f.instr(fp, dst, opts); err != nil {
return dst, err
}
if f.omitNullMarshaler && len(dst) > 4 && bytes.Compare(dst[len(dst)-4:], []byte("null")) == 0 {
dst = dst[:lastKeyOffset]
}
}
if nxt == '{' {
return append(dst, "{}"...), nil
Expand Down
5 changes: 4 additions & 1 deletion instruction.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func storeInstr(key unsafe.Pointer, instr instruction, cache instrCache) {

// newInstruction returns an instruction to encode t.
// canAddr and quoted respectively indicates if the
// value to encode is addressable and must enclosed
// value to encode is addressable and must be enclosed
// with double-quote character in the output.
func newInstruction(t reflect.Type, canAddr, quoted bool) instruction {
// Go types must be checked first, because a Duration
Expand Down Expand Up @@ -302,6 +302,9 @@ func newStructFieldsInstr(t reflect.Type, canAddr bool) instruction {
if etyp.Kind() == reflect.Ptr {
etyp = etyp.Elem()
}
if f.omitNil && (ftyp.Implements(jsonMarshalerType) || reflect.PtrTo(ftyp).Implements(jsonMarshalerType)) {
f.omitNullMarshaler = true
}
if !isNilable(ftyp) {
// Disable the omitnil option, to
// eliminate a check at runtime.
Expand Down
77 changes: 76 additions & 1 deletion json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ func TestUnsortedSyncMap(t *testing.T) {
if err := json.Unmarshal(bts, &m); err != nil {
t.Fatal(err)
}
// Unmarshaled map must contains exactly the
// Unmarshaled map must contain exactly the
// number of entries added to the sync map.
if g, w := len(m), len(entries); g != w {
t.Errorf("invalid lengths: got %d, want %d", g, w)
Expand Down Expand Up @@ -1047,6 +1047,7 @@ func TestStructFieldOmitnil(t *testing.T) {
// TestQuotedStructFields tests that the fields of
// a struct with the string option are quoted during
// marshaling if the type support it.
//
//nolint:staticcheck
func TestQuotedStructFields(t *testing.T) {
type x struct {
Expand Down Expand Up @@ -1945,3 +1946,77 @@ func TestMarshalFloat(t *testing.T) {
}
}
}

type (
jm int
jmp int
)

func (m jm) MarshalJSON() ([]byte, error) {
if m == 0 {
return []byte("null"), nil
}
return []byte(strconv.Itoa(int(m))), nil
}

func (m *jmp) MarshalJSON() ([]byte, error) {
if m == nil || *m == 0 {
return []byte("null"), nil
}
return []byte(strconv.Itoa(int(*m))), nil
}

func TestIssue5(t *testing.T) {
type X struct {
JMA jm `json:"jma,omitnil"`
JMB jm `json:"jmb,omitnil"`
JMC *jm `json:"jmc,omitnil"`
JMD *jm `json:"jmd,omitnil"`
JME jm `json:"jme"`
JMF *jm `json:"jmf"`
JMG *jm `json:"jmg,omitnil"`
JMPA jmp `json:"jmpa,omitnil"`
JMPB jmp `json:"jmpb,omitnil"`
JMPC *jmp `json:"jmpc,omitnil"`
JMPD *jmp `json:"jmpd,omitnil"`
JMPE *jmp `json:"jmpe"`
JMPF *jmp `json:"jmpf"`
JMPG jmp `json:"jmpg"`
JMPH *jmp `json:"jmph,omitnil"`
}
var (
jmc = jm(2)
jmd = jm(0)
jmpc = jmp(2)
jmpd = jmp(0)
)
x := X{
JMA: jm(4),
JMB: jm(0),
JMC: &jmc,
JMD: &jmd,
JME: jm(0),
JMF: &jmd,
JMG: nil,
JMPA: jmp(4),
// note: the JMPB field implementation of MarshalJSON
// has a pointer-receiver, but the field itself is not
// a pointer, therefore the method is not invoked and
// the omitnil option does not apply.
JMPB: jmp(0),
JMPC: &jmpc,
JMPD: &jmpd,
JMPE: nil,
JMPF: &jmpd,
JMPG: jmp(0), // same as JMPB,
JMPH: nil,
}
b, err := Marshal(x)
if err != nil {
t.Error(err)
}
want := []byte(`{"jma":4,"jmc":2,"jme":null,"jmf":null,"jmpa":4,"jmpb":0,"jmpc":2,"jmpe":null,"jmpf":null,"jmpg":0}`)
if bytes.Compare(b, want) != 0 {
t.Errorf("got %s, want %s,", string(b), string(want))
}
}
23 changes: 12 additions & 11 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ type seq struct {
}

type field struct {
typ reflect.Type
name string
keyNonEsc []byte
keyEscHTML []byte
index []int
tag bool
quoted bool
omitEmpty bool
omitNil bool
instr instruction
empty emptyFunc
typ reflect.Type
name string
keyNonEsc []byte
keyEscHTML []byte
index []int
tag bool
quoted bool
omitEmpty bool
omitNil bool
omitNullMarshaler bool
instr instruction
empty emptyFunc

// embedSeq represents the sequence of offsets
// and indirections to follow to reach the field
Expand Down

0 comments on commit 45899e5

Please sign in to comment.