cmd/viewer, types/views: implement support for json/v2 (#16852)

This adds support for having every viewer type implement
jsonv2.MarshalerTo and jsonv2.UnmarshalerFrom.

This provides a significant boost in performance
as the json package no longer needs to validate
the entirety of the JSON value outputted by MarshalJSON,
nor does it need to identify the boundaries of a JSON value
in order to call UnmarshalJSON.

For deeply nested and recursive MarshalJSON or UnmarshalJSON calls,
this can improve runtime from O(N²) to O(N).

This still references "github.com/go-json-experiment/json"
instead of the experimental "encoding/json/v2" package
now available in Go 1.25 under goexperiment.jsonv2
so that code still builds without the experiment tag.
Of note, the "github.com/go-json-experiment/json" package
aliases the standard library under the right build conditions.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
Joe Tsai
2025-08-14 13:46:48 -07:00
committed by GitHub
parent c083a9b053
commit fbb91758ac
17 changed files with 1463 additions and 201 deletions

View File

@@ -6,10 +6,12 @@
package dnstype
import (
"encoding/json"
jsonv1 "encoding/json"
"errors"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/views"
)
@@ -43,8 +45,17 @@ func (v ResolverView) AsStruct() *Resolver {
return v.ж.Clone()
}
func (v ResolverView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v ResolverView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ResolverView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ResolverView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -53,7 +64,20 @@ func (v *ResolverView) UnmarshalJSON(b []byte) error {
return nil
}
var x Resolver
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ResolverView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Resolver
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x

View File

@@ -6,9 +6,11 @@
package persist
import (
"encoding/json"
jsonv1 "encoding/json"
"errors"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/structs"
@@ -45,8 +47,17 @@ func (v PersistView) AsStruct() *Persist {
return v.ж.Clone()
}
func (v PersistView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v PersistView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v PersistView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *PersistView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -55,7 +66,20 @@ func (v *PersistView) UnmarshalJSON(b []byte) error {
return nil
}
var x Persist
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Persist
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x

View File

@@ -6,10 +6,12 @@
package prefs_example
import (
"encoding/json"
jsonv1 "encoding/json"
"errors"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
@@ -48,8 +50,17 @@ func (v PrefsView) AsStruct() *Prefs {
return v.ж.Clone()
}
func (v PrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v PrefsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v PrefsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *PrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -58,7 +69,20 @@ func (v *PrefsView) UnmarshalJSON(b []byte) error {
return nil
}
var x Prefs
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *PrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Prefs
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -160,8 +184,17 @@ func (v AutoUpdatePrefsView) AsStruct() *AutoUpdatePrefs {
return v.ж.Clone()
}
func (v AutoUpdatePrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v AutoUpdatePrefsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v AutoUpdatePrefsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *AutoUpdatePrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -170,7 +203,20 @@ func (v *AutoUpdatePrefsView) UnmarshalJSON(b []byte) error {
return nil
}
var x AutoUpdatePrefs
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *AutoUpdatePrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x AutoUpdatePrefs
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -214,8 +260,17 @@ func (v AppConnectorPrefsView) AsStruct() *AppConnectorPrefs {
return v.ж.Clone()
}
func (v AppConnectorPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v AppConnectorPrefsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v AppConnectorPrefsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *AppConnectorPrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -224,7 +279,20 @@ func (v *AppConnectorPrefsView) UnmarshalJSON(b []byte) error {
return nil
}
var x AppConnectorPrefs
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *AppConnectorPrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x AppConnectorPrefs
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x

View File

@@ -6,9 +6,12 @@
package prefs
import (
"encoding/json"
jsonv1 "encoding/json"
"errors"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup -tags=test
@@ -41,8 +44,17 @@ func (v TestPrefsView) AsStruct() *TestPrefs {
return v.ж.Clone()
}
func (v TestPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v TestPrefsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TestPrefsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TestPrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -51,7 +63,20 @@ func (v *TestPrefsView) UnmarshalJSON(b []byte) error {
return nil
}
var x TestPrefs
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TestPrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TestPrefs
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -145,8 +170,17 @@ func (v TestBundleView) AsStruct() *TestBundle {
return v.ж.Clone()
}
func (v TestBundleView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v TestBundleView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TestBundleView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TestBundleView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -155,7 +189,20 @@ func (v *TestBundleView) UnmarshalJSON(b []byte) error {
return nil
}
var x TestBundle
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TestBundleView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TestBundle
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -200,8 +247,17 @@ func (v TestValueStructView) AsStruct() *TestValueStruct {
return v.ж.Clone()
}
func (v TestValueStructView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v TestValueStructView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TestValueStructView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TestValueStructView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -210,7 +266,20 @@ func (v *TestValueStructView) UnmarshalJSON(b []byte) error {
return nil
}
var x TestValueStruct
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TestValueStructView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TestValueStruct
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -253,8 +322,17 @@ func (v TestGenericStructView[T]) AsStruct() *TestGenericStruct[T] {
return v.ж.Clone()
}
func (v TestGenericStructView[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v TestGenericStructView[T]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TestGenericStructView[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TestGenericStructView[T]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -263,7 +341,20 @@ func (v *TestGenericStructView[T]) UnmarshalJSON(b []byte) error {
return nil
}
var x TestGenericStruct[T]
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TestGenericStructView[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TestGenericStruct[T]
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
@@ -308,8 +399,17 @@ func (v TestPrefsGroupView) AsStruct() *TestPrefsGroup {
return v.ж.Clone()
}
func (v TestPrefsGroupView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v TestPrefsGroupView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TestPrefsGroupView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TestPrefsGroupView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
@@ -318,7 +418,20 @@ func (v *TestPrefsGroupView) UnmarshalJSON(b []byte) error {
return nil
}
var x TestPrefsGroup
if err := json.Unmarshal(b, &x); err != nil {
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TestPrefsGroupView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TestPrefsGroup
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x

View File

@@ -7,7 +7,7 @@ package views
import (
"bytes"
"encoding/json"
jsonv1 "encoding/json"
"errors"
"fmt"
"iter"
@@ -15,20 +15,12 @@ import (
"reflect"
"slices"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"go4.org/mem"
"tailscale.com/types/ptr"
)
func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error {
if *x != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
return json.Unmarshal(b, x)
}
// ByteSlice is a read-only accessor for types that are backed by a []byte.
type ByteSlice[T ~[]byte] struct {
// ж is the underlying mutable value, named with a hard-to-type
@@ -93,15 +85,32 @@ func (v ByteSlice[T]) SliceTo(i int) ByteSlice[T] { return ByteSlice[T]{v.ж[:i]
// Slice returns v[i:j]
func (v ByteSlice[T]) Slice(i, j int) ByteSlice[T] { return ByteSlice[T]{v.ж[i:j]} }
// MarshalJSON implements json.Marshaler.
func (v ByteSlice[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v ByteSlice[T]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// UnmarshalJSON implements json.Unmarshaler.
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ByteSlice[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized ByteSlice.
func (v *ByteSlice[T]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
return json.Unmarshal(b, &v.ж)
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized ByteSlice.
func (v *ByteSlice[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// StructView represents the corresponding StructView of a Viewable. The concrete types are
@@ -159,11 +168,35 @@ func (v SliceView[T, V]) All() iter.Seq2[int, V] {
}
}
// MarshalJSON implements json.Marshaler.
func (v SliceView[T, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
// MarshalJSON implements [jsonv1.Marshaler].
func (v SliceView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// UnmarshalJSON implements json.Unmarshaler.
func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error { return unmarshalSliceFromJSON(b, &v.ж) }
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SliceView[T, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized SliceView.
func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
} else if len(b) == 0 {
return nil
}
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized SliceView.
func (v *SliceView[T, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// IsNil reports whether the underlying slice is nil.
func (v SliceView[T, V]) IsNil() bool { return v.ж == nil }
@@ -252,14 +285,34 @@ func SliceOf[T any](x []T) Slice[T] {
return Slice[T]{x}
}
// MarshalJSON implements json.Marshaler.
// MarshalJSON implements [jsonv1.Marshaler].
func (v Slice[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(v.ж)
return jsonv1.Marshal(v.ж)
}
// UnmarshalJSON implements json.Unmarshaler.
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v Slice[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized Slice.
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
return unmarshalSliceFromJSON(b, &v.ж)
if v.ж != nil {
return errors.New("already initialized")
} else if len(b) == 0 {
return nil
}
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized Slice.
func (v *Slice[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// IsNil reports whether the underlying slice is nil.
@@ -512,18 +565,32 @@ func (m MapSlice[K, V]) GetOk(k K) (Slice[V], bool) {
return SliceOf(v), ok
}
// MarshalJSON implements json.Marshaler.
// MarshalJSON implements [jsonv1.Marshaler].
func (m MapSlice[K, V]) MarshalJSON() ([]byte, error) {
return json.Marshal(m.ж)
return jsonv1.Marshal(m.ж)
}
// UnmarshalJSON implements json.Unmarshaler.
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (m MapSlice[K, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, m.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It should only be called on an uninitialized Map.
func (m *MapSlice[K, V]) UnmarshalJSON(b []byte) error {
if m.ж != nil {
return errors.New("already initialized")
}
return json.Unmarshal(b, &m.ж)
return jsonv1.Unmarshal(b, &m.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It should only be called on an uninitialized MapSlice.
func (m *MapSlice[K, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &m.ж)
}
// AsMap returns a shallow-clone of the underlying map.
@@ -600,18 +667,32 @@ func (m Map[K, V]) GetOk(k K) (V, bool) {
return v, ok
}
// MarshalJSON implements json.Marshaler.
// MarshalJSON implements [jsonv1.Marshaler].
func (m Map[K, V]) MarshalJSON() ([]byte, error) {
return json.Marshal(m.ж)
return jsonv1.Marshal(m.ж)
}
// UnmarshalJSON implements json.Unmarshaler.
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (m Map[K, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, m.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It should only be called on an uninitialized Map.
func (m *Map[K, V]) UnmarshalJSON(b []byte) error {
if m.ж != nil {
return errors.New("already initialized")
}
return json.Unmarshal(b, &m.ж)
return jsonv1.Unmarshal(b, &m.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized Map.
func (m *Map[K, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &m.ж)
}
// AsMap returns a shallow-clone of the underlying map.
@@ -809,17 +890,32 @@ func ValuePointerOf[T any](v *T) ValuePointer[T] {
return ValuePointer[T]{v}
}
// MarshalJSON implements [json.Marshaler].
// MarshalJSON implements [jsonv1.Marshaler].
func (p ValuePointer[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(p.ж)
return jsonv1.Marshal(p.ж)
}
// UnmarshalJSON implements [json.Unmarshaler].
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p ValuePointer[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, p.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized ValuePointer.
func (p *ValuePointer[T]) UnmarshalJSON(b []byte) error {
if p.ж != nil {
return errors.New("already initialized")
}
return json.Unmarshal(b, &p.ж)
return jsonv1.Unmarshal(b, &p.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized ValuePointer.
func (p *ValuePointer[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if p.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &p.ж)
}
// ContainsPointers reports whether T contains any pointers,

View File

@@ -4,8 +4,7 @@
package views
import (
"bytes"
"encoding/json"
jsonv1 "encoding/json"
"fmt"
"net/netip"
"reflect"
@@ -15,9 +14,27 @@ import (
"unsafe"
qt "github.com/frankban/quicktest"
jsonv2 "github.com/go-json-experiment/json"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/types/structs"
)
// Statically verify that each type implements the following interfaces.
var _ = []interface {
jsonv1.Marshaler
jsonv1.Unmarshaler
jsonv2.MarshalerTo
jsonv2.UnmarshalerFrom
}{
(*ByteSlice[[]byte])(nil),
(*SliceView[*testStruct, testStructView])(nil),
(*Slice[testStruct])(nil),
(*MapSlice[*testStruct, testStructView])(nil),
(*Map[*testStruct, testStructView])(nil),
(*ValuePointer[testStruct])(nil),
}
type viewStruct struct {
Int int
Addrs Slice[netip.Prefix]
@@ -83,14 +100,16 @@ func TestViewsJSON(t *testing.T) {
ipp := SliceOf(mustCIDR("192.168.0.0/24"))
ss := SliceOf([]string{"bar"})
tests := []struct {
name string
in viewStruct
wantJSON string
name string
in viewStruct
wantJSONv1 string
wantJSONv2 string
}{
{
name: "empty",
in: viewStruct{},
wantJSON: `{"Int":0,"Addrs":null,"Strings":null}`,
name: "empty",
in: viewStruct{},
wantJSONv1: `{"Int":0,"Addrs":null,"Strings":null}`,
wantJSONv2: `{"Int":0,"Addrs":[],"Strings":[]}`,
},
{
name: "everything",
@@ -101,30 +120,49 @@ func TestViewsJSON(t *testing.T) {
StringsPtr: &ss,
Strings: ss,
},
wantJSON: `{"Int":1234,"Addrs":["192.168.0.0/24"],"Strings":["bar"],"AddrsPtr":["192.168.0.0/24"],"StringsPtr":["bar"]}`,
wantJSONv1: `{"Int":1234,"Addrs":["192.168.0.0/24"],"Strings":["bar"],"AddrsPtr":["192.168.0.0/24"],"StringsPtr":["bar"]}`,
wantJSONv2: `{"Int":1234,"Addrs":["192.168.0.0/24"],"Strings":["bar"],"AddrsPtr":["192.168.0.0/24"],"StringsPtr":["bar"]}`,
},
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetIndent("", "")
for _, tc := range tests {
buf.Reset()
if err := encoder.Encode(&tc.in); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
gotJSON := strings.TrimSpace(string(b))
if tc.wantJSON != gotJSON {
t.Fatalf("JSON: %v; want: %v", gotJSON, tc.wantJSON)
}
var got viewStruct
if err := json.Unmarshal(b, &got); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tc.in) {
t.Fatalf("unmarshal resulted in different output: %+v; want %+v", got, tc.in)
cmpOpts := cmp.Options{
cmp.AllowUnexported(Slice[string]{}),
cmp.AllowUnexported(Slice[netip.Prefix]{}),
cmpopts.EquateComparable(netip.Prefix{}),
}
t.Run("JSONv1", func(t *testing.T) {
gotJSON, err := jsonv1.Marshal(tc.in)
if err != nil {
t.Fatal(err)
}
if string(gotJSON) != tc.wantJSONv1 {
t.Fatalf("JSON: %s; want: %s", gotJSON, tc.wantJSONv1)
}
var got viewStruct
if err := jsonv1.Unmarshal(gotJSON, &got); err != nil {
t.Fatal(err)
}
if d := cmp.Diff(got, tc.in, cmpOpts); d != "" {
t.Fatalf("unmarshal mismatch (-got +want):\n%s", d)
}
})
t.Run("JSONv2", func(t *testing.T) {
gotJSON, err := jsonv2.Marshal(tc.in)
if err != nil {
t.Fatal(err)
}
if string(gotJSON) != tc.wantJSONv2 {
t.Fatalf("JSON: %s; want: %s", gotJSON, tc.wantJSONv2)
}
var got viewStruct
if err := jsonv2.Unmarshal(gotJSON, &got); err != nil {
t.Fatal(err)
}
if d := cmp.Diff(got, tc.in, cmpOpts, cmpopts.EquateEmpty()); d != "" {
t.Fatalf("unmarshal mismatch (-got +want):\n%s", d)
}
})
}
}