From c288d82b4503a785942f0e99c6e31d07dc8d6148 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:50:14 +0200 Subject: [PATCH 01/10] migrate github.com/json-iterator/go to encoding/json/v2 --- fieldpath/path.go | 2 +- fieldpath/serialize-pe.go | 184 ++++++++++++++------------- fieldpath/serialize.go | 250 +++++++++++++++++++------------------ go.mod | 11 +- go.sum | 18 +-- value/reflectcache.go | 3 +- value/reflectcache_test.go | 11 -- value/value.go | 52 ++------ 8 files changed, 248 insertions(+), 283 deletions(-) diff --git a/fieldpath/path.go b/fieldpath/path.go index a865ec42..68cdb1ee 100644 --- a/fieldpath/path.go +++ b/fieldpath/path.go @@ -80,7 +80,7 @@ func (fp Path) Copy() Path { // MakePath constructs a Path. The parts may be PathElements, ints, strings. func MakePath(parts ...interface{}) (Path, error) { - var fp Path + fp := make(Path, 0, len(parts)) for _, p := range parts { switch t := p.(type) { case PathElement: diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index f4b00c2e..9ee36426 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,13 +17,15 @@ limitations under the License. package fieldpath import ( + "bytes" "errors" "fmt" "io" "strconv" "strings" - jsoniter "github.com/json-iterator/go" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" "sigs.k8s.io/structured-merge-diff/v6/value" ) @@ -31,58 +33,85 @@ var ErrUnknownPathElementType = errors.New("unknown path element type") const ( // Field indicates that the content of this path element is a field's name - peField = "f" + peField byte = 'f' // Value indicates that the content of this path element is a field's value - peValue = "v" + peValue byte = 'v' // Index indicates that the content of this path element is an index in an array - peIndex = "i" + peIndex byte = 'i' // Key indicates that the content of this path element is a key value map - peKey = "k" + peKey byte = 'k' // Separator separates the type of a path element from the contents - peSeparator = ":" + peSeparator byte = ':' ) var ( - peFieldSepBytes = []byte(peField + peSeparator) - peValueSepBytes = []byte(peValue + peSeparator) - peIndexSepBytes = []byte(peIndex + peSeparator) - peKeySepBytes = []byte(peKey + peSeparator) - peSepBytes = []byte(peSeparator) + peFieldSepBytes = []byte{peField, peSeparator} + peValueSepBytes = []byte{peValue, peSeparator} + peIndexSepBytes = []byte{peIndex, peSeparator} + peKeySepBytes = []byte{peKey, peSeparator} ) -// readJSONIter reads a Value from a JSON iterator. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func readJSONIter(iter *jsoniter.Iterator) (value.Value, error) { - v := iter.Read() - if iter.Error != nil && iter.Error != io.EOF { - return nil, iter.Error - } - return value.NewValueInterface(v), nil +// writeValueToEncoder writes a value to an Encoder. +func writeValueToEncoder(v value.Value, enc *jsontext.Encoder) error { + return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) } -// writeJSONStream writes a value into a JSON stream. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func writeJSONStream(v value.Value, stream *jsoniter.Stream) { - stream.WriteVal(v.Unstructured()) +// FieldListFromJSON is a helper function for reading a JSON document. +func fieldListFromJSON(input []byte) (value.FieldList, error) { + parser := jsontext.NewDecoder(bytes.NewBuffer(input)) + + if objStart, err := parser.ReadToken(); err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return nil, fmt.Errorf("expected object") + } + + var fields value.FieldList + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + + var v any + if err := json.UnmarshalDecode(parser, &v); err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + fields = append(fields, value.Field{Name: k, Value: value.NewValueInterface(v)}) + } + + return fields, nil } // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { b := []byte(s) if len(b) < 2 { - return PathElement{}, errors.New("key must be 2 characters long:") + return PathElement{}, errors.New("key must be 2 characters long") } - typeSep, b := b[:2], b[2:] - if typeSep[1] != peSepBytes[0] { + typeSep0, typeSep1, b := b[0], b[1], b[2:] + if typeSep1 != peSeparator { return PathElement{}, fmt.Errorf("missing colon: %v", s) } - switch typeSep[0] { + switch typeSep0 { case peFieldSepBytes[0]: // Slice s rather than convert b, to save on // allocations. @@ -91,29 +120,18 @@ func DeserializePathElement(s string) (PathElement, error) { FieldName: &str, }, nil case peValueSepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - v, err := readJSONIter(iter) + v, err := value.FromJSON(b) if err != nil { return PathElement{}, err } return PathElement{Value: &v}, nil case peKeySepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - fields := value.FieldList{} - - iter.ReadObjectCB(func(iter *jsoniter.Iterator, key string) bool { - v, err := readJSONIter(iter) - if err != nil { - iter.Error = err - return false - } - fields = append(fields, value.Field{Name: key, Value: v}) - return true - }) + fields, err := fieldListFromJSON(b) + if err != nil { + return PathElement{}, err + } fields.Sort() - return PathElement{Key: &fields}, iter.Error + return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) if err != nil { @@ -127,60 +145,58 @@ func DeserializePathElement(s string) (PathElement, error) { } } -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) +type PathElementSerializer struct { + buffer bytes.Buffer + encoder jsontext.Encoder +} // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - buf := strings.Builder{} - err := serializePathElementToWriter(&buf, pe) - return buf.String(), err + byteVal, err := (&PathElementSerializer{}).serialize(pe) + return string(byteVal), err } -func serializePathElementToWriter(w io.Writer, pe PathElement) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) +func (pes *PathElementSerializer) serialize(pe PathElement) (string, error) { + pes.buffer.Reset() + switch { case pe.FieldName != nil: - if _, err := stream.Write(peFieldSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peFieldSepBytes); err != nil { + return "", err } - stream.WriteRaw(*pe.FieldName) + pes.buffer.WriteString(*pe.FieldName) case pe.Key != nil: - if _, err := stream.Write(peKeySepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peKeySepBytes); err != nil { + return "", err } - stream.WriteObjectStart() - - for i, field := range *pe.Key { - if i > 0 { - stream.WriteMore() + pes.encoder.Reset(&pes.buffer) + pes.encoder.WriteToken(jsontext.BeginObject) + for _, f := range *pe.Key { + if err := pes.encoder.WriteToken(jsontext.String(f.Name)); err != nil { + return "", err + } + if err := writeValueToEncoder(f.Value, &pes.encoder); err != nil { + return "", err } - stream.WriteObjectField(field.Name) - writeJSONStream(field.Value, stream) } - stream.WriteObjectEnd() + pes.encoder.WriteToken(jsontext.EndObject) case pe.Value != nil: - if _, err := stream.Write(peValueSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peValueSepBytes); err != nil { + return "", err + } + pes.encoder.Reset(&pes.buffer) + if err := writeValueToEncoder(*pe.Value, &pes.encoder); err != nil { + return "", err } - writeJSONStream(*pe.Value, stream) case pe.Index != nil: - if _, err := stream.Write(peIndexSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peIndexSepBytes); err != nil { + return "", err } - stream.WriteInt(*pe.Index) + pes.buffer.WriteString(strconv.Itoa(*pe.Index)) default: - return errors.New("invalid PathElement") + return "", errors.New("invalid PathElement") } - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return err + + // TODO: is there a way to not emit newlines + return strings.TrimSpace(pes.buffer.String()), nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index b992b93c..d593fabe 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -18,117 +18,99 @@ package fieldpath import ( "bytes" + "fmt" "io" - "unsafe" + "sort" + "sync" + "unicode" - jsoniter "github.com/json-iterator/go" + "github.com/go-json-experiment/json/jsontext" ) func (s *Set) ToJSON() ([]byte, error) { buf := bytes.Buffer{} - err := s.ToJSONStream(&buf) - if err != nil { + enc := jsontext.Encoder{} + enc.Reset(&buf) + if err := s.emitContentsV1(false, &enc); err != nil { return nil, err } - return buf.Bytes(), nil + return bytes.TrimSpace(buf.Bytes()), nil } func (s *Set) ToJSONStream(w io.Writer) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) - - var r reusableBuilder - - stream.WriteObjectStart() - err := s.emitContentsV1(false, stream, &r) - if err != nil { + buf := bytes.Buffer{} + enc := jsontext.Encoder{} + enc.Reset(&buf) + if err := s.emitContentsV1(false, &enc); err != nil { return err } - stream.WriteObjectEnd() - return stream.Flush() + bufLen := len(bytes.TrimRightFunc(buf.Bytes(), unicode.IsSpace)) + buf.Truncate(bufLen) + _, err := buf.WriteTo(w) + return err } -func manageMemory(stream *jsoniter.Stream) error { - // Help jsoniter manage its buffers--without this, it does a bunch of - // alloctaions that are not necessary. They were probably optimizing - // for folks using the buffer directly. - b := stream.Buffer() - if len(b) > 4096 || cap(b)-len(b) < 2048 { - if err := stream.Flush(); err != nil { - return err - } - stream.SetBuffer(b[:0]) - } - return nil +var pool = sync.Pool{ + New: func() any { + return &PathElementSerializer{} + }, } -type reusableBuilder struct { - bytes.Buffer -} +func writePathKey(enc *jsontext.Encoder, pe PathElement) error { + pes := pool.Get().(*PathElementSerializer) + defer pool.Put(pes) -func (r *reusableBuilder) unsafeString() string { - b := r.Bytes() - return *(*string)(unsafe.Pointer(&b)) -} + key, err := pes.serialize(pe) + if err != nil { + return err + } -func (r *reusableBuilder) reset() *bytes.Buffer { - r.Reset() - return &r.Buffer + if err := enc.WriteToken(jsontext.String(key)); err != nil { + return err + } + return nil } -func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusableBuilder) error { - mi, ci := 0, 0 - first := true - preWrite := func() { - if first { - first = false - return - } - stream.WriteMore() +func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { + if err := om.WriteToken(jsontext.BeginObject); err != nil { + return err } if includeSelf && !(len(s.Members.members) == 0 && len(s.Children.members) == 0) { - preWrite() - stream.WriteObjectField(".") - stream.WriteEmptyObject() + if err := om.WriteToken(jsontext.String(".")); err != nil { + return err + } + if err := om.WriteValue(jsontext.Value("{}")); err != nil { + return err + } } + mi, ci := 0, 0 for mi < len(s.Members.members) && ci < len(s.Children.members) { mpe := s.Members.members[mi] cpe := s.Children.members[ci].pathElement if c := mpe.Compare(cpe); c < 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { + if err := writePathKey(om, mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() - mi++ - } else if c > 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := om.WriteValue(jsontext.Value("{}")); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { - return err - } - stream.WriteObjectEnd() - ci++ + + mi++ } else { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := writePathKey(om, cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(true, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(c == 0, om); err != nil { return err } - stream.WriteObjectEnd() - mi++ + + // If we also found a member with the same path, we skip this member. + if c == 0 { + mi++ + } ci++ } } @@ -136,103 +118,131 @@ func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusa for mi < len(s.Members.members) { mpe := s.Members.members[mi] - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { + if err := writePathKey(om, mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() + if err := om.WriteValue(jsontext.Value("{}")); err != nil { + return err + } + mi++ } for ci < len(s.Children.members) { cpe := s.Children.members[ci].pathElement - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := writePathKey(om, cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(false, om); err != nil { return err } - stream.WriteObjectEnd() + ci++ } - return manageMemory(stream) + if err := om.WriteToken(jsontext.EndObject); err != nil { + return err + } + + return nil } // FromJSON clears s and reads a JSON formatted set structure. func (s *Set) FromJSON(r io.Reader) error { - // The iterator pool is completely useless for memory management, grrr. - iter := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, r, 4096) + parser := jsontext.NewDecoder(r) - found, _ := readIterV1(iter) - if found == nil { + found, _, err := readIterV1(parser) + if err != nil { + return err + } else if found == nil { *s = Set{} } else { *s = *found } - return iter.Error + return nil } // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. -func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) { - iter.ReadMapCB(func(iter *jsoniter.Iterator, key string) bool { - if key == "." { +func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { + if objStart, err := parser.ReadToken(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return nil, false, fmt.Errorf("expected object") + } + + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return nil, false, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + if k == "." { isMember = true - iter.Skip() - return true + if err := parser.SkipValue(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + continue } - pe, err := DeserializePathElement(key) + pe, err := DeserializePathElement(k) if err == ErrUnknownPathElementType { // Ignore these-- a future version maybe knows what // they are. We drop these completely rather than try // to preserve things we don't understand. - iter.Skip() - return true + if err := parser.SkipValue(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + continue } else if err != nil { - iter.ReportError("parsing key as path element", err.Error()) - iter.Skip() - return true + return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - grandchildren, childIsMember := readIterV1(iter) - if childIsMember { + + grandChildren, isChildMember, err := readIterV1(parser) + if err != nil { + return nil, false, fmt.Errorf("parsing value as set: %v", err) + } + + if isChildMember { if children == nil { children = &Set{} } + + // Append the member to the members list, we will sort it later m := &children.Members.members - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. - appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe) - if appendOK { - *m = append(*m, pe) - } else { - children.Members.Insert(pe) - } + *m = append(*m, pe) } - if grandchildren != nil { + + if grandChildren != nil { if children == nil { children = &Set{} } - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. + + // Append the child to the children list, we will sort it later m := &children.Children.members - appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe) - if appendOK { - *m = append(*m, setNode{pe, grandchildren}) - } else { - *children.Children.Descend(pe) = *grandchildren - } + *m = append(*m, setNode{pe, grandChildren}) } - return true - }) + } + + // Sort the members and children + if children != nil { + sort.Sort(children.Members.members) + sort.Sort(children.Children.members) + } + if children == nil { isMember = true } - return children, isMember + return children, isMember, nil } diff --git a/go.mod b/go.mod index f5343b69..bb9f5b5d 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,10 @@ module sigs.k8s.io/structured-merge-diff/v6 +go 1.24 + require ( + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 github.com/google/go-cmp v0.5.9 - github.com/json-iterator/go v1.1.12 go.yaml.in/yaml/v2 v2.4.2 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 ) - -require ( - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect -) - -go 1.23 diff --git a/go.sum b/go.sum index e120aad1..70b2d04e 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,7 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/value/reflectcache.go b/value/reflectcache.go index 3b4a402e..97162af5 100644 --- a/value/reflectcache.go +++ b/value/reflectcache.go @@ -18,7 +18,6 @@ package value import ( "bytes" - "encoding/json" "errors" "fmt" "io" @@ -26,6 +25,8 @@ import ( "sort" "sync" "sync/atomic" + + "encoding/json" ) // UnstructuredConverter defines how a type can be converted directly to unstructured. diff --git a/value/reflectcache_test.go b/value/reflectcache_test.go index c1a7c856..0d51374e 100644 --- a/value/reflectcache_test.go +++ b/value/reflectcache_test.go @@ -17,7 +17,6 @@ limitations under the License. package value import ( - "encoding/json" "fmt" "reflect" "testing" @@ -327,16 +326,6 @@ func TestUnmarshal(t *testing.T) { Want: map[string]interface{}{}, WantError: true, }, - { - JSON: `1.0`, - IntoType: reflect.TypeOf(json.Number("")), - Want: json.Number("1.0"), - }, - { - JSON: `1`, - IntoType: reflect.TypeOf(json.Number("")), - Want: json.Number("1"), - }, { JSON: `1.0`, IntoType: reflect.TypeOf(float64(0)), diff --git a/value/value.go b/value/value.go index 140b9903..2b38ab11 100644 --- a/value/value.go +++ b/value/value.go @@ -19,19 +19,12 @@ package value import ( "bytes" "fmt" - "io" "strings" - jsoniter "github.com/json-iterator/go" - + "github.com/go-json-experiment/json" yaml "go.yaml.in/yaml/v2" ) -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) - // A Value corresponds to an 'atom' in the schema. It should return true // for at least one of the IsXXX methods below, or the value is // considered "invalid" @@ -84,48 +77,23 @@ type Value interface { // FromJSON is a helper function for reading a JSON document. func FromJSON(input []byte) (Value, error) { - return FromJSONFast(input) + var v any + if err := json.Unmarshal(input, &v); err != nil { + return nil, err + } + + return NewValueInterface(v), nil } // FromJSONFast is a helper function for reading a JSON document. func FromJSONFast(input []byte) (Value, error) { - iter := readPool.BorrowIterator(input) - defer readPool.ReturnIterator(iter) - return readJSONIter(iter) + return FromJSON(input) } // ToJSON is a helper function for producing a JSon document. func ToJSON(v Value) ([]byte, error) { - buf := bytes.Buffer{} - stream := writePool.BorrowStream(&buf) - defer writePool.ReturnStream(stream) - writeJSONStream(v, stream) - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return buf.Bytes(), err -} - -// readJSONIter reads a Value from a JSON iterator. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func readJSONIter(iter *jsoniter.Iterator) (Value, error) { - v := iter.Read() - if iter.Error != nil && iter.Error != io.EOF { - return nil, iter.Error - } - return NewValueInterface(v), nil -} - -// writeJSONStream writes a value into a JSON stream. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func writeJSONStream(v Value, stream *jsoniter.Stream) { - stream.WriteVal(v.Unstructured()) + jsonBytes, err := json.Marshal(v.Unstructured(), json.Deterministic(true)) + return bytes.TrimSpace(jsonBytes), err } // ToYAML marshals a value as YAML. From 99ecf291a4b56797836b79d665b49616da9a4800 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 08:59:02 +0200 Subject: [PATCH 02/10] use MarshalJSONTo function Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 47 +++++++++------------------------------ fieldpath/serialize.go | 46 +++++++++++--------------------------- value/fields.go | 18 +++++++++++++++ value/value.go | 4 +--- 4 files changed, 43 insertions(+), 72 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 9ee36426..14f3c236 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -55,11 +55,6 @@ var ( peKeySepBytes = []byte{peKey, peSeparator} ) -// writeValueToEncoder writes a value to an Encoder. -func writeValueToEncoder(v value.Value, enc *jsontext.Encoder) error { - return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) -} - // FieldListFromJSON is a helper function for reading a JSON document. func fieldListFromJSON(input []byte) (value.FieldList, error) { parser := jsontext.NewDecoder(bytes.NewBuffer(input)) @@ -145,58 +140,38 @@ func DeserializePathElement(s string) (PathElement, error) { } } -type PathElementSerializer struct { - buffer bytes.Buffer - encoder jsontext.Encoder -} - // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - byteVal, err := (&PathElementSerializer{}).serialize(pe) - return string(byteVal), err -} - -func (pes *PathElementSerializer) serialize(pe PathElement) (string, error) { - pes.buffer.Reset() + builder := strings.Builder{} switch { case pe.FieldName != nil: - if _, err := pes.buffer.Write(peFieldSepBytes); err != nil { + if _, err := builder.Write(peFieldSepBytes); err != nil { return "", err } - pes.buffer.WriteString(*pe.FieldName) + builder.WriteString(*pe.FieldName) case pe.Key != nil: - if _, err := pes.buffer.Write(peKeySepBytes); err != nil { + if _, err := builder.Write(peKeySepBytes); err != nil { return "", err } - pes.encoder.Reset(&pes.buffer) - pes.encoder.WriteToken(jsontext.BeginObject) - for _, f := range *pe.Key { - if err := pes.encoder.WriteToken(jsontext.String(f.Name)); err != nil { - return "", err - } - if err := writeValueToEncoder(f.Value, &pes.encoder); err != nil { - return "", err - } + if err := json.MarshalWrite(&builder, *pe.Key, json.Deterministic(true)); err != nil { + return "", err } - pes.encoder.WriteToken(jsontext.EndObject) case pe.Value != nil: - if _, err := pes.buffer.Write(peValueSepBytes); err != nil { + if _, err := builder.Write(peValueSepBytes); err != nil { return "", err } - pes.encoder.Reset(&pes.buffer) - if err := writeValueToEncoder(*pe.Value, &pes.encoder); err != nil { + if err := json.MarshalWrite(&builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { return "", err } case pe.Index != nil: - if _, err := pes.buffer.Write(peIndexSepBytes); err != nil { + if _, err := builder.Write(peIndexSepBytes); err != nil { return "", err } - pes.buffer.WriteString(strconv.Itoa(*pe.Index)) + builder.WriteString(strconv.Itoa(*pe.Index)) default: return "", errors.New("invalid PathElement") } - // TODO: is there a way to not emit newlines - return strings.TrimSpace(pes.buffer.String()), nil + return builder.String(), nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index d593fabe..2ce18d54 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,50 +17,24 @@ limitations under the License. package fieldpath import ( - "bytes" "fmt" "io" "sort" - "sync" - "unicode" + "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" ) func (s *Set) ToJSON() ([]byte, error) { - buf := bytes.Buffer{} - enc := jsontext.Encoder{} - enc.Reset(&buf) - if err := s.emitContentsV1(false, &enc); err != nil { - return nil, err - } - return bytes.TrimSpace(buf.Bytes()), nil + return json.Marshal((*setContentsV1)(s)) } func (s *Set) ToJSONStream(w io.Writer) error { - buf := bytes.Buffer{} - enc := jsontext.Encoder{} - enc.Reset(&buf) - if err := s.emitContentsV1(false, &enc); err != nil { - return err - } - bufLen := len(bytes.TrimRightFunc(buf.Bytes(), unicode.IsSpace)) - buf.Truncate(bufLen) - _, err := buf.WriteTo(w) - return err -} - -var pool = sync.Pool{ - New: func() any { - return &PathElementSerializer{} - }, + return json.MarshalWrite(w, (*setContentsV1)(s)) } func writePathKey(enc *jsontext.Encoder, pe PathElement) error { - pes := pool.Get().(*PathElementSerializer) - defer pool.Put(pes) - - key, err := pes.serialize(pe) + key, err := SerializePathElement(pe) if err != nil { return err } @@ -71,7 +45,13 @@ func writePathKey(enc *jsontext.Encoder, pe PathElement) error { return nil } -func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { +type setContentsV1 Set + +func (s *setContentsV1) MarshalJSONTo(enc *jsontext.Encoder) error { + return s.emitContentsV1(false, enc) +} + +func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := om.WriteToken(jsontext.BeginObject); err != nil { return err } @@ -103,7 +83,7 @@ func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := writePathKey(om, cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(c == 0, om); err != nil { + if err := (*setContentsV1)(s.Children.members[ci].set).emitContentsV1(c == 0, om); err != nil { return err } @@ -134,7 +114,7 @@ func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := writePathKey(om, cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(false, om); err != nil { + if err := (*setContentsV1)(s.Children.members[ci].set).emitContentsV1(false, om); err != nil { return err } diff --git a/value/fields.go b/value/fields.go index 042b0487..8a05566b 100644 --- a/value/fields.go +++ b/value/fields.go @@ -19,6 +19,9 @@ package value import ( "sort" "strings" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" ) // Field is an individual key-value pair. @@ -31,6 +34,21 @@ type Field struct { // have a different name. type FieldList []Field +func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { + enc.WriteToken(jsontext.BeginObject) + for _, f := range fl { + if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { + return err + } + if err := json.MarshalEncode(enc, f.Value.Unstructured(), json.Deterministic(true)); err != nil { + return err + } + } + enc.WriteToken(jsontext.EndObject) + + return nil +} + // Copy returns a copy of the FieldList. // Values are not copied. func (f FieldList) Copy() FieldList { diff --git a/value/value.go b/value/value.go index 2b38ab11..f6128bbe 100644 --- a/value/value.go +++ b/value/value.go @@ -17,7 +17,6 @@ limitations under the License. package value import ( - "bytes" "fmt" "strings" @@ -92,8 +91,7 @@ func FromJSONFast(input []byte) (Value, error) { // ToJSON is a helper function for producing a JSon document. func ToJSON(v Value) ([]byte, error) { - jsonBytes, err := json.Marshal(v.Unstructured(), json.Deterministic(true)) - return bytes.TrimSpace(jsonBytes), err + return json.Marshal(v.Unstructured(), json.Deterministic(true)) } // ToYAML marshals a value as YAML. From 03d95ec12acc6ad7a2fe2f3e45b6682a80155055 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:17:48 +0200 Subject: [PATCH 03/10] use UnmarshalJSONFrom function Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 49 ++------------------------------------- fieldpath/serialize.go | 20 +++++++++------- value/fields.go | 48 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 14f3c236..e037b7d2 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,15 +17,12 @@ limitations under the License. package fieldpath import ( - "bytes" "errors" "fmt" - "io" "strconv" "strings" "github.com/go-json-experiment/json" - "github.com/go-json-experiment/json/jsontext" "sigs.k8s.io/structured-merge-diff/v6/value" ) @@ -55,47 +52,6 @@ var ( peKeySepBytes = []byte{peKey, peSeparator} ) -// FieldListFromJSON is a helper function for reading a JSON document. -func fieldListFromJSON(input []byte) (value.FieldList, error) { - parser := jsontext.NewDecoder(bytes.NewBuffer(input)) - - if objStart, err := parser.ReadToken(); err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } else if objStart.Kind() != jsontext.BeginObject.Kind() { - return nil, fmt.Errorf("expected object") - } - - var fields value.FieldList - for { - if parser.PeekKind() == jsontext.EndObject.Kind() { - if _, err := parser.ReadToken(); err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - break - } - - rawKey, err := parser.ReadToken() - if err == io.EOF { - return nil, fmt.Errorf("unexpected EOF") - } else if err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - - k := rawKey.String() - - var v any - if err := json.UnmarshalDecode(parser, &v); err == io.EOF { - return nil, fmt.Errorf("unexpected EOF") - } else if err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - - fields = append(fields, value.Field{Name: k, Value: value.NewValueInterface(v)}) - } - - return fields, nil -} - // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { b := []byte(s) @@ -121,11 +77,10 @@ func DeserializePathElement(s string) (PathElement, error) { } return PathElement{Value: &v}, nil case peKeySepBytes[0]: - fields, err := fieldListFromJSON(b) - if err != nil { + var fields value.FieldList + if err := json.Unmarshal(b, &fields); err != nil { return PathElement{}, err } - fields.Sort() return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index 2ce18d54..ac101276 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -128,24 +128,21 @@ func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) e return nil } -// FromJSON clears s and reads a JSON formatted set structure. -func (s *Set) FromJSON(r io.Reader) error { - parser := jsontext.NewDecoder(r) - - found, _, err := readIterV1(parser) +func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + found, _, err := s.readIterV1(dec) if err != nil { return err } else if found == nil { - *s = Set{} + *(*Set)(s) = Set{} } else { - *s = *found + *(*Set)(s) = *found } return nil } // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. -func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { +func (s *setContentsV1) readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { if objStart, err := parser.ReadToken(); err != nil { return nil, false, fmt.Errorf("parsing JSON: %v", err) } else if objStart.Kind() != jsontext.BeginObject.Kind() { @@ -188,7 +185,7 @@ func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err err return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - grandChildren, isChildMember, err := readIterV1(parser) + grandChildren, isChildMember, err := s.readIterV1(parser) if err != nil { return nil, false, fmt.Errorf("parsing value as set: %v", err) } @@ -226,3 +223,8 @@ func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err err return children, isMember, nil } + +// FromJSON clears s and reads a JSON formatted set structure. +func (s *Set) FromJSON(r io.Reader) error { + return json.UnmarshalRead(r, (*setContentsV1)(s)) +} diff --git a/value/fields.go b/value/fields.go index 8a05566b..e62c24cb 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,6 +17,8 @@ limitations under the License. package value import ( + "fmt" + "io" "sort" "strings" @@ -34,9 +36,9 @@ type Field struct { // have a different name. type FieldList []Field -func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { +func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { enc.WriteToken(jsontext.BeginObject) - for _, f := range fl { + for _, f := range *fl { if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { return err } @@ -49,6 +51,48 @@ func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { return nil } +// FieldListFromJSON is a helper function for reading a JSON document. +func (fl *FieldList) UnmarshalJSONFrom(parser *jsontext.Decoder) error { + if objStart, err := parser.ReadToken(); err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return fmt.Errorf("expected object") + } + + var fields FieldList + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return fmt.Errorf("unexpected EOF") + } else if err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + + var v any + if err := json.UnmarshalDecode(parser, &v); err == io.EOF { + return fmt.Errorf("unexpected EOF") + } else if err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + + fields = append(fields, Field{Name: k, Value: NewValueInterface(v)}) + } + + fields.Sort() + *fl = fields + + return nil +} + // Copy returns a copy of the FieldList. // Values are not copied. func (f FieldList) Copy() FieldList { From 7290eb63330cd5508e7333c171de15829a5f289a Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 10:56:06 +0200 Subject: [PATCH 04/10] peformance tuning Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 39 ++++++++++++++++++++++++--------------- fieldpath/serialize.go | 19 ++++++++++++++++--- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index e037b7d2..1021acf3 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,10 +17,10 @@ limitations under the License. package fieldpath import ( + "bytes" "errors" "fmt" "strconv" - "strings" "github.com/go-json-experiment/json" "sigs.k8s.io/structured-merge-diff/v6/value" @@ -97,36 +97,45 @@ func DeserializePathElement(s string) (PathElement, error) { // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - builder := strings.Builder{} + builder := bytes.Buffer{} + if err := serializePathElementBuilder(pe, &builder); err != nil { + return "", err + } + return builder.String(), nil +} +func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { switch { case pe.FieldName != nil: if _, err := builder.Write(peFieldSepBytes); err != nil { - return "", err + return err + } + if _, err := builder.WriteString(*pe.FieldName); err != nil { + return err } - builder.WriteString(*pe.FieldName) case pe.Key != nil: if _, err := builder.Write(peKeySepBytes); err != nil { - return "", err + return err } - if err := json.MarshalWrite(&builder, *pe.Key, json.Deterministic(true)); err != nil { - return "", err + if err := json.MarshalWrite(builder, pe.Key, json.Deterministic(true)); err != nil { + return err } case pe.Value != nil: if _, err := builder.Write(peValueSepBytes); err != nil { - return "", err + return err } - if err := json.MarshalWrite(&builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { - return "", err + if err := json.MarshalWrite(builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { + return err } case pe.Index != nil: if _, err := builder.Write(peIndexSepBytes); err != nil { - return "", err + return err + } + if _, err := builder.WriteString(strconv.Itoa(*pe.Index)); err != nil { + return err } - builder.WriteString(strconv.Itoa(*pe.Index)) default: - return "", errors.New("invalid PathElement") + return errors.New("invalid PathElement") } - - return builder.String(), nil + return nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index ac101276..a1b8774c 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,9 +17,11 @@ limitations under the License. package fieldpath import ( + "bytes" "fmt" "io" "sort" + "sync" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" @@ -33,13 +35,24 @@ func (s *Set) ToJSONStream(w io.Writer) error { return json.MarshalWrite(w, (*setContentsV1)(s)) } +var pool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} + func writePathKey(enc *jsontext.Encoder, pe PathElement) error { - key, err := SerializePathElement(pe) - if err != nil { + builder := pool.Get().(*bytes.Buffer) + defer func() { + builder.Reset() + pool.Put(builder) + }() + + if err := serializePathElementBuilder(pe, builder); err != nil { return err } - if err := enc.WriteToken(jsontext.String(key)); err != nil { + if err := enc.WriteToken(jsontext.String(builder.String())); err != nil { return err } return nil From 7580c7efcfd77d850e6881eff77e9c921aa167c5 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:22:42 +0200 Subject: [PATCH 05/10] introduce MarshalValue Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 2 +- value/fields.go | 58 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 1021acf3..dc61ff29 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -124,7 +124,7 @@ func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { if _, err := builder.Write(peValueSepBytes); err != nil { return err } - if err := json.MarshalWrite(builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { + if err := json.MarshalWrite(builder, value.MarshalValue{Value: pe.Value}, json.Deterministic(true)); err != nil { return err } case pe.Index != nil: diff --git a/value/fields.go b/value/fields.go index e62c24cb..13d1b546 100644 --- a/value/fields.go +++ b/value/fields.go @@ -32,6 +32,62 @@ type Field struct { Value Value } +type MarshalValue struct { + Value *Value +} + +func (mv MarshalValue) MarshalJSONTo(enc *jsontext.Encoder) error { + return valueMarshalJSONTo(enc, *mv.Value) +} + +func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { + switch { + case v.IsNull(): + return enc.WriteToken(jsontext.Null) + case v.IsFloat(): + return enc.WriteToken(jsontext.Float(v.AsFloat())) + case v.IsInt(): + return enc.WriteToken(jsontext.Int(v.AsInt())) + case v.IsString(): + return enc.WriteToken(jsontext.String(v.AsString())) + case v.IsBool(): + return enc.WriteToken(jsontext.Bool(v.AsBool())) + case v.IsList(): + if err := enc.WriteToken(jsontext.BeginArray); err != nil { + return err + } + list := v.AsList() + for i := 0; i < list.Length(); i++ { + if err := valueMarshalJSONTo(enc, list.At(i)); err != nil { + return err + } + } + return enc.WriteToken(jsontext.EndArray) + case v.IsMap(): + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err + } + var iterErr error + v.AsMap().Iterate(func(k string, v Value) bool { + if err := enc.WriteToken(jsontext.String(k)); err != nil { + iterErr = err + return false + } + if err := valueMarshalJSONTo(enc, v); err != nil { + iterErr = err + return false + } + return true + }) + if iterErr != nil { + return iterErr + } + return enc.WriteToken(jsontext.EndObject) + default: + return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) + } +} + // FieldList is a list of key-value pairs. Each field is expected to // have a different name. type FieldList []Field @@ -42,7 +98,7 @@ func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { return err } - if err := json.MarshalEncode(enc, f.Value.Unstructured(), json.Deterministic(true)); err != nil { + if err := valueMarshalJSONTo(enc, f.Value); err != nil { return err } } From 3f1e73c2ebb874c04ae3a09318032cb6d85aed34 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Fri, 29 Aug 2025 22:23:37 +0200 Subject: [PATCH 06/10] upgrade github.com/go-json-experiment/json Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bb9f5b5d..2032d7e4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module sigs.k8s.io/structured-merge-diff/v6 go 1.24 require ( - github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 + github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b github.com/google/go-cmp v0.5.9 go.yaml.in/yaml/v2 v2.4.2 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 diff --git a/go.sum b/go.sum index 70b2d04e..cd8ce4a2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU= +github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= From b2387eaf45b56ab911e3bdf02c61a4ebaa7836ef Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:25:51 +0200 Subject: [PATCH 07/10] reduce serialize allocations Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 46 +++++++++++++++++++++++---------------- fieldpath/serialize.go | 13 +++++------ 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index dc61ff29..5a7931a9 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -54,31 +54,28 @@ var ( // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { - b := []byte(s) - if len(b) < 2 { + if len(s) < 2 { return PathElement{}, errors.New("key must be 2 characters long") } - typeSep0, typeSep1, b := b[0], b[1], b[2:] + typeSep0, typeSep1, rest := s[0], s[1], s[2:] if typeSep1 != peSeparator { return PathElement{}, fmt.Errorf("missing colon: %v", s) } switch typeSep0 { case peFieldSepBytes[0]: - // Slice s rather than convert b, to save on - // allocations. str := s[2:] return PathElement{ FieldName: &str, }, nil case peValueSepBytes[0]: - v, err := value.FromJSON(b) + v, err := value.FromJSON([]byte(rest)) if err != nil { return PathElement{}, err } return PathElement{Value: &v}, nil case peKeySepBytes[0]: var fields value.FieldList - if err := json.Unmarshal(b, &fields); err != nil { + if err := json.Unmarshal([]byte(rest), &fields); err != nil { return PathElement{}, err } return PathElement{Key: &fields}, nil @@ -97,41 +94,52 @@ func DeserializePathElement(s string) (PathElement, error) { // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - builder := bytes.Buffer{} - if err := serializePathElementBuilder(pe, &builder); err != nil { + serializer := pathElementSerializer{} + if err := serializer.serialize(pe); err != nil { return "", err } - return builder.String(), nil + return serializer.builder.String(), nil } -func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { +type pathElementSerializer struct { + builder bytes.Buffer + fastValue value.MarshalValue +} + +func (pes *pathElementSerializer) reset() { + pes.builder.Reset() + pes.fastValue.Value = nil +} + +func (pes *pathElementSerializer) serialize(pe PathElement) error { switch { case pe.FieldName != nil: - if _, err := builder.Write(peFieldSepBytes); err != nil { + if _, err := pes.builder.Write(peFieldSepBytes); err != nil { return err } - if _, err := builder.WriteString(*pe.FieldName); err != nil { + if _, err := pes.builder.WriteString(*pe.FieldName); err != nil { return err } case pe.Key != nil: - if _, err := builder.Write(peKeySepBytes); err != nil { + if _, err := pes.builder.Write(peKeySepBytes); err != nil { return err } - if err := json.MarshalWrite(builder, pe.Key, json.Deterministic(true)); err != nil { + if err := json.MarshalWrite(&pes.builder, pe.Key, json.Deterministic(true)); err != nil { return err } case pe.Value != nil: - if _, err := builder.Write(peValueSepBytes); err != nil { + if _, err := pes.builder.Write(peValueSepBytes); err != nil { return err } - if err := json.MarshalWrite(builder, value.MarshalValue{Value: pe.Value}, json.Deterministic(true)); err != nil { + pes.fastValue.Value = pe.Value + if err := json.MarshalWrite(&pes.builder, &pes.fastValue, json.Deterministic(true)); err != nil { return err } case pe.Index != nil: - if _, err := builder.Write(peIndexSepBytes); err != nil { + if _, err := pes.builder.Write(peIndexSepBytes); err != nil { return err } - if _, err := builder.WriteString(strconv.Itoa(*pe.Index)); err != nil { + if _, err := pes.builder.WriteString(strconv.Itoa(*pe.Index)); err != nil { return err } default: diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index a1b8774c..b46850a2 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,7 +17,6 @@ limitations under the License. package fieldpath import ( - "bytes" "fmt" "io" "sort" @@ -37,22 +36,22 @@ func (s *Set) ToJSONStream(w io.Writer) error { var pool = sync.Pool{ New: func() any { - return &bytes.Buffer{} + return &pathElementSerializer{} }, } func writePathKey(enc *jsontext.Encoder, pe PathElement) error { - builder := pool.Get().(*bytes.Buffer) + serializer := pool.Get().(*pathElementSerializer) defer func() { - builder.Reset() - pool.Put(builder) + serializer.reset() + pool.Put(serializer) }() - if err := serializePathElementBuilder(pe, builder); err != nil { + if err := serializer.serialize(pe); err != nil { return err } - if err := enc.WriteToken(jsontext.String(builder.String())); err != nil { + if err := enc.WriteToken(jsontext.String(serializer.builder.String())); err != nil { return err } return nil From 27e265be657660a027f579a6c53145aee217e8db Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:27:24 +0200 Subject: [PATCH 08/10] improve sorting Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/element.go | 14 +++----------- fieldpath/serialize.go | 10 +++++++--- fieldpath/set.go | 20 ++++++-------------- value/fields.go | 6 +++--- 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/fieldpath/element.go b/fieldpath/element.go index 73436912..697c061d 100644 --- a/fieldpath/element.go +++ b/fieldpath/element.go @@ -226,27 +226,19 @@ func KeyByFields(nameValues ...interface{}) *value.FieldList { // PathElementSet is a set of path elements. // TODO: serialize as a list. type PathElementSet struct { - members sortedPathElements + members []PathElement } func MakePathElementSet(size int) PathElementSet { return PathElementSet{ - members: make(sortedPathElements, 0, size), + members: make([]PathElement, 0, size), } } -type sortedPathElements []PathElement - -// Implement the sort interface; this would permit bulk creation, which would -// be faster than doing it one at a time via Insert. -func (spe sortedPathElements) Len() int { return len(spe) } -func (spe sortedPathElements) Less(i, j int) bool { return spe[i].Less(spe[j]) } -func (spe sortedPathElements) Swap(i, j int) { spe[i], spe[j] = spe[j], spe[i] } - // Copy returns a copy of the PathElementSet. // This is not a full deep copy as any contained value.Value is not copied. func (s PathElementSet) Copy() PathElementSet { - out := make(sortedPathElements, len(s.members)) + out := make([]PathElement, len(s.members)) for i := range s.members { out[i] = s.members[i].Copy() } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index b46850a2..0919646d 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -19,7 +19,7 @@ package fieldpath import ( "fmt" "io" - "sort" + "slices" "sync" "github.com/go-json-experiment/json" @@ -225,8 +225,12 @@ func (s *setContentsV1) readIterV1(parser *jsontext.Decoder) (children *Set, isM // Sort the members and children if children != nil { - sort.Sort(children.Members.members) - sort.Sort(children.Children.members) + slices.SortFunc(children.Members.members, func(a, b PathElement) int { + return a.Compare(b) + }) + slices.SortFunc(children.Children.members, func(a, b setNode) int { + return a.pathElement.Compare(b.pathElement) + }) } if children == nil { diff --git a/fieldpath/set.go b/fieldpath/set.go index d2d8c8a4..7b280b69 100644 --- a/fieldpath/set.go +++ b/fieldpath/set.go @@ -131,7 +131,7 @@ func (s *Set) RecursiveDifference(s2 *Set) *Set { // "a" if it's a named fields but not "a.b" if it's a map. func (s *Set) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *Set { members := PathElementSet{ - members: make(sortedPathElements, 0, s.Members.Size()+len(s.Children.members)), + members: make([]PathElement, 0, s.Members.Size()+len(s.Children.members)), } atom, _ := sc.Resolve(tr) members.members = append(members.members, s.Members.members...) @@ -463,21 +463,13 @@ type setNode struct { // SetNodeMap is a map of PathElement to subset. type SetNodeMap struct { - members sortedSetNode + members []setNode } -type sortedSetNode []setNode - -// Implement the sort interface; this would permit bulk creation, which would -// be faster than doing it one at a time via Insert. -func (s sortedSetNode) Len() int { return len(s) } -func (s sortedSetNode) Less(i, j int) bool { return s[i].pathElement.Less(s[j].pathElement) } -func (s sortedSetNode) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - // Copy returns a copy of the SetNodeMap. // This is not a full deep copy as any contained value.Value is not copied. func (s *SetNodeMap) Copy() SetNodeMap { - out := make(sortedSetNode, len(s.members)) + out := make([]setNode, len(s.members)) for i, v := range s.members { out[i] = setNode{pathElement: v.pathElement.Copy(), set: v.set.Copy()} } @@ -677,7 +669,7 @@ func (s *SetNodeMap) RecursiveDifference(s2 *Set) *SetNodeMap { // EnsureNamedFieldsAreMembers returns a set that contains all the named fields along with the leaves. func (s *SetNodeMap) EnsureNamedFieldsAreMembers(sc *schema.Schema, tr schema.TypeRef) *SetNodeMap { - out := make(sortedSetNode, 0, s.Size()) + out := make([]setNode, 0, s.Size()) atom, _ := sc.Resolve(tr) for _, member := range s.members { tr := schema.TypeRef{} @@ -706,7 +698,7 @@ func (s *SetNodeMap) FilterIncludeMatches(pattern *SetMatcher) *SetNodeMap { return s } - var out sortedSetNode + var out []setNode for _, member := range s.members { for _, c := range pattern.members { if c.Path.Wildcard || c.Path.PathElement.Equals(member.pathElement) { @@ -754,7 +746,7 @@ func (s *SetNodeMap) iteratePrefix(prefix Path, f func(Path)) { // only setNodes with leaf PathElements. func (s *SetNodeMap) Leaves() *SetNodeMap { out := &SetNodeMap{} - out.members = make(sortedSetNode, len(s.members)) + out.members = make([]setNode, len(s.members)) for i, n := range s.members { out.members[i] = setNode{ pathElement: n.pathElement, diff --git a/value/fields.go b/value/fields.go index 13d1b546..1bdc381f 100644 --- a/value/fields.go +++ b/value/fields.go @@ -19,7 +19,7 @@ package value import ( "fmt" "io" - "sort" + "slices" "strings" "github.com/go-json-experiment/json" @@ -168,8 +168,8 @@ func (f FieldList) Sort() { } return } - sort.SliceStable(f, func(i, j int) bool { - return f[i].Name < f[j].Name + slices.SortStableFunc(f, func(a, b Field) int { + return strings.Compare(a.Name, b.Name) }) } From ef02f1b6c377de199c6f1c34c3718265db1e136f Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:28:00 +0200 Subject: [PATCH 09/10] cleanup serialisation Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 12 +++++++----- value/fields.go | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 5a7931a9..a80291c4 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "strconv" + "strings" "github.com/go-json-experiment/json" "sigs.k8s.io/structured-merge-diff/v6/value" @@ -68,14 +69,15 @@ func DeserializePathElement(s string) (PathElement, error) { FieldName: &str, }, nil case peValueSepBytes[0]: - v, err := value.FromJSON([]byte(rest)) - if err != nil { + var v any + if err := json.UnmarshalRead(strings.NewReader(rest), &v); err != nil { return PathElement{}, err } - return PathElement{Value: &v}, nil + interfaceValue := value.NewValueInterface(v) + return PathElement{Value: &interfaceValue}, nil case peKeySepBytes[0]: var fields value.FieldList - if err := json.Unmarshal([]byte(rest), &fields); err != nil { + if err := json.UnmarshalRead(strings.NewReader(rest), &fields); err != nil { return PathElement{}, err } return PathElement{Key: &fields}, nil @@ -103,7 +105,7 @@ func SerializePathElement(pe PathElement) (string, error) { type pathElementSerializer struct { builder bytes.Buffer - fastValue value.MarshalValue + fastValue value.FastMarshalValue } func (pes *pathElementSerializer) reset() { diff --git a/value/fields.go b/value/fields.go index 1bdc381f..648cf92a 100644 --- a/value/fields.go +++ b/value/fields.go @@ -32,11 +32,12 @@ type Field struct { Value Value } -type MarshalValue struct { +// Not meant to be used by an external library. +type FastMarshalValue struct { Value *Value } -func (mv MarshalValue) MarshalJSONTo(enc *jsontext.Encoder) error { +func (mv FastMarshalValue) MarshalJSONTo(enc *jsontext.Encoder) error { return valueMarshalJSONTo(enc, *mv.Value) } From 8273db415281d117376643df2325c1fff36a8c41 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:17:25 +0200 Subject: [PATCH 10/10] upgrade github.com/go-json-experiment/json Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize.go | 15 +++++++++------ go.mod | 2 +- go.sum | 4 ++-- value/fields.go | 27 ++++++++++++++++----------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index 0919646d..78e7c8f8 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -59,12 +59,15 @@ func writePathKey(enc *jsontext.Encoder, pe PathElement) error { type setContentsV1 Set -func (s *setContentsV1) MarshalJSONTo(enc *jsontext.Encoder) error { +var _ json.MarshalerTo = (*setContentsV1)(nil) +var _ json.UnmarshalerFrom = (*setContentsV1)(nil) + +func (s *setContentsV1) MarshalJSONTo(enc *jsontext.Encoder, _ jsontext.Options) error { return s.emitContentsV1(false, enc) } func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { - if err := om.WriteToken(jsontext.BeginObject); err != nil { + if err := om.WriteToken(jsontext.ObjectStart); err != nil { return err } @@ -133,14 +136,14 @@ func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) e ci++ } - if err := om.WriteToken(jsontext.EndObject); err != nil { + if err := om.WriteToken(jsontext.ObjectEnd); err != nil { return err } return nil } -func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder) error { +func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder, _ jsontext.Options) error { found, _, err := s.readIterV1(dec) if err != nil { return err @@ -157,12 +160,12 @@ func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder) error { func (s *setContentsV1) readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { if objStart, err := parser.ReadToken(); err != nil { return nil, false, fmt.Errorf("parsing JSON: %v", err) - } else if objStart.Kind() != jsontext.BeginObject.Kind() { + } else if objStart.Kind() != jsontext.ObjectStart.Kind() { return nil, false, fmt.Errorf("expected object") } for { - if parser.PeekKind() == jsontext.EndObject.Kind() { + if parser.PeekKind() == jsontext.ObjectEnd.Kind() { if _, err := parser.ReadToken(); err != nil { return nil, false, fmt.Errorf("parsing JSON: %v", err) } diff --git a/go.mod b/go.mod index 2032d7e4..6b8f3f96 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module sigs.k8s.io/structured-merge-diff/v6 go 1.24 require ( - github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b + github.com/go-json-experiment/json v0.0.0-20250211222650-7564cc53b040 github.com/google/go-cmp v0.5.9 go.yaml.in/yaml/v2 v2.4.2 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 diff --git a/go.sum b/go.sum index cd8ce4a2..255f7c9d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU= -github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250211222650-7564cc53b040 h1:trEF1NteT0pHrGrW6AK9RBanU9VMQDnT3Xi0oQU2Jso= +github.com/go-json-experiment/json v0.0.0-20250211222650-7564cc53b040/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= diff --git a/value/fields.go b/value/fields.go index 648cf92a..89d54fd5 100644 --- a/value/fields.go +++ b/value/fields.go @@ -37,7 +37,9 @@ type FastMarshalValue struct { Value *Value } -func (mv FastMarshalValue) MarshalJSONTo(enc *jsontext.Encoder) error { +var _ json.MarshalerTo = FastMarshalValue{} + +func (mv FastMarshalValue) MarshalJSONTo(enc *jsontext.Encoder, _ jsontext.Options) error { return valueMarshalJSONTo(enc, *mv.Value) } @@ -54,7 +56,7 @@ func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { case v.IsBool(): return enc.WriteToken(jsontext.Bool(v.AsBool())) case v.IsList(): - if err := enc.WriteToken(jsontext.BeginArray); err != nil { + if err := enc.WriteToken(jsontext.ArrayStart); err != nil { return err } list := v.AsList() @@ -63,9 +65,9 @@ func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { return err } } - return enc.WriteToken(jsontext.EndArray) + return enc.WriteToken(jsontext.ArrayEnd) case v.IsMap(): - if err := enc.WriteToken(jsontext.BeginObject); err != nil { + if err := enc.WriteToken(jsontext.ObjectStart); err != nil { return err } var iterErr error @@ -83,7 +85,7 @@ func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { if iterErr != nil { return iterErr } - return enc.WriteToken(jsontext.EndObject) + return enc.WriteToken(jsontext.ObjectEnd) default: return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) } @@ -93,8 +95,11 @@ func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { // have a different name. type FieldList []Field -func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { - enc.WriteToken(jsontext.BeginObject) +var _ json.MarshalerTo = (*FieldList)(nil) +var _ json.UnmarshalerFrom = (*FieldList)(nil) + +func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder, _ jsontext.Options) error { + enc.WriteToken(jsontext.ObjectStart) for _, f := range *fl { if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { return err @@ -103,22 +108,22 @@ func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { return err } } - enc.WriteToken(jsontext.EndObject) + enc.WriteToken(jsontext.ObjectEnd) return nil } // FieldListFromJSON is a helper function for reading a JSON document. -func (fl *FieldList) UnmarshalJSONFrom(parser *jsontext.Decoder) error { +func (fl *FieldList) UnmarshalJSONFrom(parser *jsontext.Decoder, _ jsontext.Options) error { if objStart, err := parser.ReadToken(); err != nil { return fmt.Errorf("parsing JSON: %v", err) - } else if objStart.Kind() != jsontext.BeginObject.Kind() { + } else if objStart.Kind() != jsontext.ObjectStart.Kind() { return fmt.Errorf("expected object") } var fields FieldList for { - if parser.PeekKind() == jsontext.EndObject.Kind() { + if parser.PeekKind() == jsontext.ObjectEnd.Kind() { if _, err := parser.ReadToken(); err != nil { return fmt.Errorf("parsing JSON: %v", err) }