diff --git a/src/encoding/json/encode.go b/src/encoding/json/encode.go index 5b67251fbb8595..dba44a70fbed83 100644 --- a/src/encoding/json/encode.go +++ b/src/encoding/json/encode.go @@ -746,18 +746,39 @@ FieldLoop: fv = fv.Field(i) } + // fast path omitEmpty if f.omitEmpty && isEmptyValue(fv) { continue } + omitEmptyResetLocation := e.Len() + e.WriteByte(next) - next = ',' if opts.escapeHTML { e.WriteString(f.nameEscHTML) } else { e.WriteString(f.nameNonEsc) } opts.quoted = f.quoted + + startLen := e.Len() f.encoder(e, fv, opts) + newLen := e.Len() + + if f.omitEmpty && (newLen-startLen) <= 5 { + // using `Next` and `bytes.NewBuffer` we can modify the end of the + // underlying slice efficiently (without doing any copying) + fullBuf := e.Next(newLen) // extract underlying slice from buffer + switch string(fullBuf[startLen:newLen]) { + case "false", "0", "\"\"", "null", "[]": + // reconstruct buffer without zero value + e.Buffer = *bytes.NewBuffer(fullBuf[:omitEmptyResetLocation]) + continue + default: + // reconstruct original buffer + e.Buffer = *bytes.NewBuffer(fullBuf) + } + } + next = ',' } if next == '{' { e.WriteString("{}") diff --git a/src/encoding/json/encode_test.go b/src/encoding/json/encode_test.go index 0b021f0074991d..26054ed023e561 100644 --- a/src/encoding/json/encode_test.go +++ b/src/encoding/json/encode_test.go @@ -17,6 +17,12 @@ import ( "unicode" ) +type NullMarshal int + +func (NullMarshal) MarshalJSON() ([]byte, error) { + return []byte(`null`), nil +} + type Optionals struct { Sr string `json:"sr"` So string `json:"so,omitempty"` @@ -42,6 +48,9 @@ type Optionals struct { Str struct{} `json:"str"` Sto struct{} `json:"sto,omitempty"` + + Nur NullMarshal `json:"nur"` + Nuo NullMarshal `json:"nuo,omitempty"` } var optionalsExpected = `{ @@ -53,7 +62,8 @@ var optionalsExpected = `{ "br": false, "ur": 0, "str": {}, - "sto": {} + "sto": {}, + "nur": null }` func TestOmitEmpty(t *testing.T) { @@ -1082,9 +1092,9 @@ func TestMarshalRawMessageValue(t *testing.T) { {map[string]any{"M": &rawNil}, `{"M":null}`, true}, {&map[string]any{"M": &rawNil}, `{"M":null}`, true}, {T1{rawNil}, "{}", true}, - {T2{&rawNil}, `{"M":null}`, true}, + {T2{&rawNil}, "{}", true}, {&T1{rawNil}, "{}", true}, - {&T2{&rawNil}, `{"M":null}`, true}, + {&T2{&rawNil}, "{}", true}, // Test with empty, but non-nil, RawMessage. {rawEmpty, "", false},