diff --git a/core/build_info.go b/core/build_info.go index cb36f8ecc4..edf6c600be 100644 --- a/core/build_info.go +++ b/core/build_info.go @@ -2,7 +2,7 @@ package core import ( "context" - "encoding/json" + "encoding/json/v2" "fmt" "io" "net/http" diff --git a/core/error.go b/core/error.go index baef3c8784..ca312b1560 100644 --- a/core/error.go +++ b/core/error.go @@ -1,7 +1,8 @@ package core import ( - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "fmt" "strings" @@ -70,11 +71,28 @@ func (s *CliError) MarshalHuman() (string, error) { return strings.Join(sections, "\n\n"), nil } +// jsonError returns the JSON representation of e, reproducing the encoding/json v1 +// behavior: an error is marshaled through its exported fields or MarshalJSON, and an +// error with no exported fields (e.g. errors.errorString) marshals to {}. +func jsonError(e error) string { + b, err := json.Marshal(e, jsontext.EscapeForHTML(true)) + if err != nil { + return "{}" + } + + return string(b) +} + +// jsonQuoted returns s as a JSON string literal. +func jsonQuoted(s string) (string, error) { + b, err := json.Marshal(s, jsontext.EscapeForHTML(true)) + + return string(b), err +} + func (s *CliError) MarshalJSON() ([]byte, error) { if s.Empty { - type emptyRes struct{} - - return json.Marshal(&emptyRes{}) + return []byte("{}"), nil } message := s.Message @@ -82,17 +100,36 @@ func (s *CliError) MarshalJSON() ([]byte, error) { message = s.Err.Error() } - type tmpRes struct { - Message string `json:"message,omitempty"` - Error error `json:"error,omitempty"` - Details string `json:"details,omitempty"` - Hint string `json:"hint,omitempty"` + // encoding/json v2 refuses to marshal errors with no exported fields and its + // omitempty drops them, so the fields are assembled by hand to keep the v1 wire format. + parts := make([]string, 0, 4) + if message != "" { + q, err := jsonQuoted(message) + if err != nil { + return nil, err + } + + parts = append(parts, `"message":`+q) + } + if s.Err != nil { + parts = append(parts, `"error":`+jsonError(s.Err)) + } + if s.Details != "" { + q, err := jsonQuoted(s.Details) + if err != nil { + return nil, err + } + + parts = append(parts, `"details":`+q) + } + if s.Hint != "" { + q, err := jsonQuoted(s.Hint) + if err != nil { + return nil, err + } + + parts = append(parts, `"hint":`+q) } - return json.Marshal(&tmpRes{ - Message: message, - Error: s.Err, - Details: s.Details, - Hint: s.Hint, - }) + return []byte("{" + strings.Join(parts, ",") + "}"), nil } diff --git a/core/example.go b/core/example.go index 7eec4ed0c9..13c4d8e523 100644 --- a/core/example.go +++ b/core/example.go @@ -1,7 +1,7 @@ package core import ( - "encoding/json" + "encoding/json/v2" "fmt" "strings" diff --git a/core/human/marshal_func.go b/core/human/marshal_func.go index 9cffd61e05..b65cd9ad83 100644 --- a/core/human/marshal_func.go +++ b/core/human/marshal_func.go @@ -1,7 +1,7 @@ package human import ( - "encoding/json" + "encoding/json/v2" "fmt" "net" "reflect" diff --git a/core/printer.go b/core/printer.go index 16dcd341cd..e52b78e344 100644 --- a/core/printer.go +++ b/core/printer.go @@ -1,7 +1,10 @@ package core import ( - "encoding/json" + "encoding" + jsonv1 "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "fmt" "io" @@ -14,6 +17,12 @@ import ( "github.com/scaleway/scaleway-cli/v2/internal/gofields" ) +// OmitEmptyLegacy is an encoding/json/v2 option that preserves the legacy +// (v1) omitempty semantics: a non-nil pointer to an empty struct is not +// considered empty. This keeps SDK union markers such as +// "load_balancer": {} or "public_network": {} in the JSON output. +var OmitEmptyLegacy = jsonv1.OmitEmptyWithLegacySemantics(true) + // Type defines an formatter format. type PrinterType string @@ -284,9 +293,15 @@ func (p *Printer) printJSON(data any) error { if isError { writer = p.stderr } - encoder := json.NewEncoder(writer) + opts := []json.Options{ + json.Deterministic(true), + json.FormatNilSliceAsNull(true), + json.FormatNilMapAsNull(true), + jsontext.EscapeForHTML(true), + OmitEmptyLegacy, + } if p.jsonPretty { - encoder.SetIndent("", " ") + opts = append(opts, jsontext.WithIndent(" ")) } // We handle special case to make sure that a nil slice is marshal as `[]` @@ -296,7 +311,57 @@ func (p *Printer) printJSON(data any) error { return err } - return encoder.Encode(data) + // encoding/json v2 refuses to marshal structs with no exported fields + // (e.g. *bytes.Buffer), where v1 encoded them as {}. + if noExportedFields(data) { + _, err := writer.Write([]byte("{}\n")) + + return err + } + + if err := json.MarshalWrite(writer, data, opts...); err != nil { + return err + } + _, err = io.WriteString(writer, "\n") + + return err +} + +var ( + marshalJSONType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() + marshalTextType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() +) + +// noExportedFields reports whether v (or the value it points to) is a struct +// with no exported fields and no JSON or text marshaling hook. +func noExportedFields(v any) bool { + rv := reflect.ValueOf(v) + for rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface { + if rv.IsNil() { + return false + } + rv = rv.Elem() + } + if rv.Kind() != reflect.Struct { + return false + } + t := rv.Type() + for f := range t.Fields() { + if f.PkgPath == "" { + return false + } + } + + // encoding/json v2 can still marshal such a struct when a JSON or text + // marshaling hook is available (e.g. time.Time, netip.Addr), + // so only fall back to {} when no hook exists. + if t.Implements(marshalJSONType) || t.Implements(marshalTextType) || + reflect.PointerTo(t).Implements(marshalJSONType) || + reflect.PointerTo(t).Implements(marshalTextType) { + return false + } + + return true } func (p *Printer) printYAML(data any) error { diff --git a/core/printer_test.go b/core/printer_test.go index 4fdbffe571..a1e0935d54 100644 --- a/core/printer_test.go +++ b/core/printer_test.go @@ -1,9 +1,12 @@ package core_test import ( + "bytes" "context" + "net/netip" "reflect" "testing" + "time" "github.com/scaleway/scaleway-cli/v2/core" ) @@ -240,3 +243,66 @@ func Test_TemplatePrinter(t *testing.T) { Check: core.TestCheckGolden(), })) } + +func Test_JSONPrinter(t *testing.T) { + type Empty struct{} + type Endpoint struct { + ID string `json:"id"` + LoadBalancer *Empty `json:"load_balancer,omitempty"` + } + + commands := core.NewCommands( + &core.Command{ + Namespace: "time", + ArgsType: reflect.TypeOf(struct{}{}), + Run: func(_ context.Context, _ any) (any, error) { + return time.Date(2026, 11, 3, 12, 0, 0, 0, time.UTC), nil + }, + }, + &core.Command{ + Namespace: "addr", + ArgsType: reflect.TypeOf(struct{}{}), + Run: func(_ context.Context, _ any) (any, error) { + return netip.MustParseAddr("127.0.0.1"), nil + }, + }, + &core.Command{ + Namespace: "buffer", + ArgsType: reflect.TypeOf(struct{}{}), + Run: func(_ context.Context, _ any) (any, error) { + return &bytes.Buffer{}, nil + }, + }, + &core.Command{ + Namespace: "endpoint", + ArgsType: reflect.TypeOf(struct{}{}), + Run: func(_ context.Context, _ any) (any, error) { + return Endpoint{ID: "111111111", LoadBalancer: &Empty{}}, nil + }, + }, + ) + + t.Run("time", core.Test(&core.TestConfig{ + Commands: commands, + Cmd: "scw time -o json", + Check: core.TestCheckGolden(), + })) + + t.Run("addr", core.Test(&core.TestConfig{ + Commands: commands, + Cmd: "scw addr -o json", + Check: core.TestCheckGolden(), + })) + + t.Run("buffer", core.Test(&core.TestConfig{ + Commands: commands, + Cmd: "scw buffer -o json", + Check: core.TestCheckGolden(), + })) + + t.Run("union-marker", core.Test(&core.TestConfig{ + Commands: commands, + Cmd: "scw endpoint -o json", + Check: core.TestCheckGolden(), + })) +} diff --git a/core/result.go b/core/result.go index a5e2e67081..af43b0effe 100644 --- a/core/result.go +++ b/core/result.go @@ -1,7 +1,7 @@ package core import ( - "encoding/json" + "encoding/json/v2" "fmt" "strings" diff --git a/core/testdata/test-json-printer-addr.cassette.yaml b/core/testdata/test-json-printer-addr.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/core/testdata/test-json-printer-addr.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/core/testdata/test-json-printer-addr.golden b/core/testdata/test-json-printer-addr.golden new file mode 100644 index 0000000000..7f82823936 --- /dev/null +++ b/core/testdata/test-json-printer-addr.golden @@ -0,0 +1,5 @@ +🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 +🟩🟩🟩 STDOUT️ 🟩🟩🟩️ +"127.0.0.1" +🟩🟩🟩 JSON STDOUT 🟩🟩🟩 +"127.0.0.1" diff --git a/core/testdata/test-json-printer-buffer.cassette.yaml b/core/testdata/test-json-printer-buffer.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/core/testdata/test-json-printer-buffer.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/core/testdata/test-json-printer-buffer.golden b/core/testdata/test-json-printer-buffer.golden new file mode 100644 index 0000000000..b6fb3cc9f2 --- /dev/null +++ b/core/testdata/test-json-printer-buffer.golden @@ -0,0 +1,5 @@ +🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 +🟩🟩🟩 STDOUT️ 🟩🟩🟩️ +{} +🟩🟩🟩 JSON STDOUT 🟩🟩🟩 +{} diff --git a/core/testdata/test-json-printer-time.cassette.yaml b/core/testdata/test-json-printer-time.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/core/testdata/test-json-printer-time.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/core/testdata/test-json-printer-time.golden b/core/testdata/test-json-printer-time.golden new file mode 100644 index 0000000000..37eb79109a --- /dev/null +++ b/core/testdata/test-json-printer-time.golden @@ -0,0 +1,5 @@ +🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 +🟩🟩🟩 STDOUT️ 🟩🟩🟩️ +"2026-11-03T12:00:00Z" +🟩🟩🟩 JSON STDOUT 🟩🟩🟩 +"2026-11-03T12:00:00Z" diff --git a/core/testdata/test-json-printer-union-marker.cassette.yaml b/core/testdata/test-json-printer-union-marker.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/core/testdata/test-json-printer-union-marker.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/core/testdata/test-json-printer-union-marker.golden b/core/testdata/test-json-printer-union-marker.golden new file mode 100644 index 0000000000..4572de446d --- /dev/null +++ b/core/testdata/test-json-printer-union-marker.golden @@ -0,0 +1,8 @@ +🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 +🟩🟩🟩 STDOUT️ 🟩🟩🟩️ +{"id":"111111111","load_balancer":{}} +🟩🟩🟩 JSON STDOUT 🟩🟩🟩 +{ + "id": "111111111", + "load_balancer": {} +} diff --git a/core/testing.go b/core/testing.go index 1059cec5db..a08105e3d9 100644 --- a/core/testing.go +++ b/core/testing.go @@ -2,7 +2,7 @@ package core import ( "bytes" - "encoding/json" + "encoding/json/v2" "flag" "fmt" "io" @@ -326,7 +326,6 @@ var foldersUsingVCRv4 = []string{ "internal/namespaces/flexibleip/v1alpha1", "internal/namespaces/iam/v1alpha1", "internal/namespaces/info", - "internal/namespaces/init", "internal/namespaces/instance/v1", "internal/namespaces/instance/v2alpha1", "internal/namespaces/iot/v1", @@ -436,6 +435,13 @@ func Test(config *TestConfig) func(t *testing.T) { overrideEnv = map[string]string{} } + // Scrub ambient KUBECONFIG so tests relying on the default $HOME/.kube/config + // path are not affected by the host environment. Tests that need it set it + // explicitly via TestConfig.OverrideEnv. + if _, ok := overrideEnv["KUBECONFIG"]; !ok { + overrideEnv["KUBECONFIG"] = "" + } + if config.TmpHomeDir { dir := t.TempDir() overrideEnv["HOME"] = dir @@ -1009,6 +1015,7 @@ func marshalGolden(t *testing.T, ctx *CheckFuncCtx) string { str := buffer.String() // In order to avoid diff in goldens we set all timestamp to the same date str = uniformTimestamps(str) + str = uniformTimestampsWithOffSet(str) // Replace Windows return carriage. str = strings.ReplaceAll(str, "\r", "") diff --git a/internal/editor/marshal.go b/internal/editor/marshal.go index 34b99210ab..9c62fd0dde 100644 --- a/internal/editor/marshal.go +++ b/internal/editor/marshal.go @@ -1,7 +1,8 @@ package editor import ( - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "fmt" "strings" @@ -32,7 +33,7 @@ func marshal(i any, mode MarshalMode) ([]byte, error) { case MarshalModeYAML: marshaledData, err = yaml.Marshal(i) case MarshalModeJSON: - marshaledData, err = json.MarshalIndent(i, "", " ") + marshaledData, err = json.Marshal(i, jsontext.WithIndent(" ")) } if err != nil { return marshaledData, err diff --git a/internal/namespaces/container/v1/custom_deploy.go b/internal/namespaces/container/v1/custom_deploy.go index 8b6d1bc582..b2a6ee64e7 100644 --- a/internal/namespaces/container/v1/custom_deploy.go +++ b/internal/namespaces/container/v1/custom_deploy.go @@ -5,7 +5,7 @@ package container import ( "context" "encoding/base64" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" diff --git a/internal/namespaces/container/v1/custom_logs.go b/internal/namespaces/container/v1/custom_logs.go index eb614e432b..3bd062b30f 100644 --- a/internal/namespaces/container/v1/custom_logs.go +++ b/internal/namespaces/container/v1/custom_logs.go @@ -2,7 +2,7 @@ package container import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" diff --git a/internal/namespaces/inference/v1/custom_serverless_models.go b/internal/namespaces/inference/v1/custom_serverless_models.go index d22a1aafdf..13ae1900a5 100644 --- a/internal/namespaces/inference/v1/custom_serverless_models.go +++ b/internal/namespaces/inference/v1/custom_serverless_models.go @@ -2,7 +2,7 @@ package inference import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "net/http" @@ -202,7 +202,7 @@ func fetchModels(ctx context.Context, secret string) (*ListServerlessModelsRespo } var listResp ListServerlessModelsResponse - if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { + if err := json.UnmarshalRead(resp.Body, &listResp); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } diff --git a/internal/namespaces/k8s/v1/custom_execcredentials.go b/internal/namespaces/k8s/v1/custom_execcredentials.go index 6eaa7c7155..d07639925a 100644 --- a/internal/namespaces/k8s/v1/custom_execcredentials.go +++ b/internal/namespaces/k8s/v1/custom_execcredentials.go @@ -2,7 +2,8 @@ package k8s import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "reflect" "github.com/scaleway/scaleway-cli/v2/core" @@ -41,7 +42,7 @@ func k8sExecCredentialRun(ctx context.Context, _ any) (i any, e error) { Token: secretKey, }, } - response, err := json.MarshalIndent(execCreds, "", " ") + response, err := json.Marshal(execCreds, jsontext.WithIndent(" ")) if err != nil { return nil, err } diff --git a/internal/namespaces/k8s/v1/custom_execcredentials_test.go b/internal/namespaces/k8s/v1/custom_execcredentials_test.go index 23b1fb2b92..2dd1ed3274 100644 --- a/internal/namespaces/k8s/v1/custom_execcredentials_test.go +++ b/internal/namespaces/k8s/v1/custom_execcredentials_test.go @@ -1,7 +1,7 @@ package k8s_test import ( - "encoding/json" + "encoding/json/v2" "os" "path" "testing" diff --git a/internal/namespaces/login/login.go b/internal/namespaces/login/login.go index daa6c3bf89..7a30cb2a55 100644 --- a/internal/namespaces/login/login.go +++ b/internal/namespaces/login/login.go @@ -3,7 +3,7 @@ package login import ( "context" "encoding/base64" - "encoding/json" + "encoding/json/v2" "fmt" "net/url" "reflect" diff --git a/internal/namespaces/mcp/server/mcp_resources.go b/internal/namespaces/mcp/server/mcp_resources.go index 07c26f2225..90f6157d3f 100644 --- a/internal/namespaces/mcp/server/mcp_resources.go +++ b/internal/namespaces/mcp/server/mcp_resources.go @@ -2,7 +2,8 @@ package server import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "fmt" "net/url" "reflect" @@ -189,7 +190,11 @@ func (cr *CommandResource) Execute( // Format the result var resultStr string if result != nil { - if data, err := json.MarshalIndent(result, "", " "); err == nil { + if data, err := json.Marshal( + result, + core.OmitEmptyLegacy, + jsontext.WithIndent(" "), + ); err == nil { resultStr = string(data) } else { resultStr = fmt.Sprintf("%v", result) diff --git a/internal/namespaces/mcp/server/mcp_tools.go b/internal/namespaces/mcp/server/mcp_tools.go index 7b8a26a6b6..134baea576 100644 --- a/internal/namespaces/mcp/server/mcp_tools.go +++ b/internal/namespaces/mcp/server/mcp_tools.go @@ -2,7 +2,8 @@ package server import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "fmt" "maps" @@ -230,7 +231,11 @@ func (ct *CommandTool) Execute( result = reflect.MakeSlice(v.Type(), 0, 0).Interface() } - if data, err := json.MarshalIndent(result, "", " "); err == nil { + if data, err := json.Marshal( + result, + core.OmitEmptyLegacy, + jsontext.WithIndent(" "), + ); err == nil { resultStr = string(data) } else { resultStr = fmt.Sprintf("%v", result) diff --git a/internal/namespaces/mcp/server/mcp_tools_test.go b/internal/namespaces/mcp/server/mcp_tools_test.go index 78bfafbbd7..027b298c58 100644 --- a/internal/namespaces/mcp/server/mcp_tools_test.go +++ b/internal/namespaces/mcp/server/mcp_tools_test.go @@ -2,7 +2,8 @@ package server_test import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "reflect" "strings" "testing" @@ -325,7 +326,7 @@ func TestToolMetaSerialization(t *testing.T) { mcpTool := tool.ToMCPTool() // Marshal to JSON to see what the client actually receives - jsonBytes, err := json.MarshalIndent(mcpTool, "", " ") + jsonBytes, err := json.Marshal(mcpTool, jsontext.WithIndent(" ")) if err != nil { t.Fatalf("Failed to marshal tool: %v", err) } diff --git a/internal/namespaces/mnq/v1beta1/custom_nats_helpers.go b/internal/namespaces/mnq/v1beta1/custom_nats_helpers.go index 1f05f02751..f18bfc4047 100644 --- a/internal/namespaces/mnq/v1beta1/custom_nats_helpers.go +++ b/internal/namespaces/mnq/v1beta1/custom_nats_helpers.go @@ -2,7 +2,7 @@ package mnq import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "os" diff --git a/internal/namespaces/object/v1/custom_bucket_cors.go b/internal/namespaces/object/v1/custom_bucket_cors.go index f1438e75f2..71924d5a27 100644 --- a/internal/namespaces/object/v1/custom_bucket_cors.go +++ b/internal/namespaces/object/v1/custom_bucket_cors.go @@ -4,7 +4,7 @@ package object import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "os" diff --git a/internal/namespaces/object/v1/custom_bucket_lifecycle.go b/internal/namespaces/object/v1/custom_bucket_lifecycle.go index af2215c03c..78b7f1fba7 100644 --- a/internal/namespaces/object/v1/custom_bucket_lifecycle.go +++ b/internal/namespaces/object/v1/custom_bucket_lifecycle.go @@ -4,7 +4,7 @@ package object import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "os" diff --git a/internal/namespaces/object/v1/custom_bucket_policy.go b/internal/namespaces/object/v1/custom_bucket_policy.go index d9a7582f95..f54baf400f 100644 --- a/internal/namespaces/object/v1/custom_bucket_policy.go +++ b/internal/namespaces/object/v1/custom_bucket_policy.go @@ -3,9 +3,8 @@ package object import ( - "bytes" "context" - "encoding/json" + "encoding/json/jsontext" "errors" "fmt" "os" @@ -203,8 +202,8 @@ func bucketPolicyGetCommand() *core.Command { return nil, fmt.Errorf("could not get bucket policy: %w", err) } - var prettyJSON bytes.Buffer - err = json.Indent(&prettyJSON, []byte(*policyResponse.Policy), "", " ") + prettyJSON := jsontext.Value([]byte(*policyResponse.Policy)) + err = prettyJSON.Indent(jsontext.WithIndent(" ")) if err != nil { return nil, fmt.Errorf("error indenting JSON: %w", err) } diff --git a/internal/namespaces/object/v1/custom_marshaler.go b/internal/namespaces/object/v1/custom_marshaler.go index 8ed57606a3..4a4558fc34 100644 --- a/internal/namespaces/object/v1/custom_marshaler.go +++ b/internal/namespaces/object/v1/custom_marshaler.go @@ -1,11 +1,12 @@ package object import ( - "bytes" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "strings" "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go/middleware" "github.com/scaleway/scaleway-cli/v2/core" "github.com/scaleway/scaleway-cli/v2/core/human" "github.com/scaleway/scaleway-sdk-go/scw" @@ -139,8 +140,14 @@ func bucketMarshalerFunc(i any, opt *human.MarshalOpt) (string, error) { // CleanAndIndentJSON takes any struct or data, marshals it, // strips all top-level and nested "null" fields, and returns formatted JSON. +var awsMetadataMarshalers = json.WithMarshalers( + json.MarshalFunc(func(middleware.Metadata) ([]byte, error) { + return []byte("{}"), nil + }), +) + func CleanAndIndentJSON(v any, prefix, indent string) ([]byte, error) { - rawBytes, err := json.Marshal(v) + rawBytes, err := json.Marshal(v, awsMetadataMarshalers, json.Deterministic(true)) if err != nil { return nil, err } @@ -152,17 +159,12 @@ func CleanAndIndentJSON(v any, prefix, indent string) ([]byte, error) { cleanedData := removeNulls(genericData) - cleanBytes, err := json.Marshal(cleanedData) - if err != nil { - return nil, err - } - - var prettyBuf bytes.Buffer - if err := json.Indent(&prettyBuf, cleanBytes, prefix, indent); err != nil { - return nil, err - } - - return prettyBuf.Bytes(), nil + return json.Marshal( + cleanedData, + json.Deterministic(true), + jsontext.WithIndentPrefix(prefix), + jsontext.WithIndent(indent), + ) } // removeNulls recursively purges nil entries from maps and slices diff --git a/internal/namespaces/object/v1/s3configfile.go b/internal/namespaces/object/v1/s3configfile.go index 8f8ec59c0e..8f814843a3 100644 --- a/internal/namespaces/object/v1/s3configfile.go +++ b/internal/namespaces/object/v1/s3configfile.go @@ -5,7 +5,7 @@ package object import ( "bytes" "context" - "encoding/json" + "encoding/json/v2" "errors" "path" "strings" diff --git a/internal/namespaces/registry/v1/custom_docker_helper.go b/internal/namespaces/registry/v1/custom_docker_helper.go index 1278c01f39..2e87596c47 100644 --- a/internal/namespaces/registry/v1/custom_docker_helper.go +++ b/internal/namespaces/registry/v1/custom_docker_helper.go @@ -4,7 +4,7 @@ import ( "bufio" "bytes" "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -262,7 +262,7 @@ func registryDockerHelperListCommand() *core.Command { for _, region := range scw.AllRegions { registryEndpoints[getRegistryEndpoint(region)] = "scaleway" } - raw, err := json.Marshal(registryEndpoints) + raw, err := json.Marshal(registryEndpoints, json.Deterministic(true)) if err != nil { return nil, err } diff --git a/internal/namespaces/registry/v1/helpers_docker.go b/internal/namespaces/registry/v1/helpers_docker.go index 35dae480b3..b43620af1b 100644 --- a/internal/namespaces/registry/v1/helpers_docker.go +++ b/internal/namespaces/registry/v1/helpers_docker.go @@ -2,7 +2,8 @@ package registry import ( "context" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "fmt" "io" @@ -98,14 +99,12 @@ func setupDockerConfigFile(ctx context.Context, registries []string, binaryName return err } - encoder := json.NewEncoder(f) - encoder.SetIndent("", " ") - err = encoder.Encode(dockerConfig) - if err != nil { + if err := json.MarshalWrite(f, dockerConfig, jsontext.WithIndent(" ")); err != nil { return err } + _, err = f.WriteString("\n") - return nil + return err } func getRegistryEndpoint(region scw.Region) string { diff --git a/internal/namespaces/secret/v1beta1/custom_secret_version.go b/internal/namespaces/secret/v1beta1/custom_secret_version.go index 797fc5820a..e702139fd2 100644 --- a/internal/namespaces/secret/v1beta1/custom_secret_version.go +++ b/internal/namespaces/secret/v1beta1/custom_secret_version.go @@ -2,7 +2,7 @@ package secret import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "reflect" diff --git a/internal/qa/example.go b/internal/qa/example.go index 491814a613..4f0f943e22 100644 --- a/internal/qa/example.go +++ b/internal/qa/example.go @@ -1,7 +1,7 @@ package qa import ( - "encoding/json" + "encoding/json/v2" "fmt" "github.com/scaleway/scaleway-cli/v2/core"