(experimental) upgraded to encoding/json/v2

This commit is contained in:
Gani Georgiev committed 2026-08-18 18:16:22 +03:00
1 parent 4d4275c9aa
commit 97dd775455
125 files changed
+433 -364

No files matched your search

+10 -10
View File
@@ -2,7 +2,7 @@ package router_test
import (
"database/sql"
"encoding/json"
"encoding/json/v2"
"errors"
"fmt"
"io/fs"
@@ -22,7 +22,7 @@ func TestNewApiErrorWithRawData(t *testing.T) {
"rawData_test",
)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
expected := `{"data":{},"message":"Message_test.","status":300}`
if string(result) != expected {
@@ -61,7 +61,7 @@ func TestNewApiErrorWithValidationData(t *testing.T) {
},
)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
expected := `{"data":{"err1":{"code":"validation_invalid_value","message":"Invalid value."},"err2":{"code":"validation_required","message":"Cannot be blank."},"err3":{"err3.1":{"code":"validation_invalid_value","message":"Invalid value."},"err3.2":{"code":"validation_required","message":"Cannot be blank."},"err3.3":{"err3.3.1":{"code":"validation_required","message":"Cannot be blank."}}},"err4":{"code":"mock_code","message":"Mock_error.","mock_resolve":123},"err5":{"err5.1":{"code":"validation_required","message":"Cannot be blank."}}},"message":"Message_test.","status":300}`
if string(result) != expected {
@@ -93,7 +93,7 @@ func TestNewNotFoundError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewNotFoundError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -118,7 +118,7 @@ func TestNewBadRequestError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewBadRequestError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -143,7 +143,7 @@ func TestNewForbiddenError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewForbiddenError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -168,7 +168,7 @@ func TestNewUnauthorizedError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewUnauthorizedError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -193,7 +193,7 @@ func TestNewInternalServerError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewInternalServerError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -218,7 +218,7 @@ func TestNewTooManyRequestsError(t *testing.T) {
for i, s := range scenarios {
t.Run(strconv.Itoa(i), func(t *testing.T) {
e := router.NewTooManyRequestsError(s.message, s.data)
result, _ := json.Marshal(e)
result, _ := json.Marshal(e, json.Deterministic(true))
if str := string(result); str != s.expected {
t.Fatalf("Expected\n%v\ngot\n%v", s.expected, str)
@@ -321,7 +321,7 @@ func TestToApiError(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
raw, err := json.Marshal(router.ToApiError(s.err))
raw, err := json.Marshal(router.ToApiError(s.err), json.Deterministic(true))
if err != nil {
t.Fatal(err)
}
+20 -6
View File
@@ -1,7 +1,8 @@
package router
import (
"encoding/json"
"encoding/json/jsontext"
"encoding/json/v2"
"encoding/xml"
"errors"
"io"
@@ -192,7 +193,7 @@ func (e *Event) JSON(status int, data any) error {
// error response or no fields to pick
if rawFields == "" || status < 200 || status > 299 {
return json.NewEncoder(e.Response).Encode(data)
return json.MarshalWrite(e.Response, data)
}
// pick only the requested fields
@@ -201,7 +202,7 @@ func (e *Event) JSON(status int, data any) error {
return err
}
return json.NewEncoder(e.Response).Encode(modified)
return json.MarshalWrite(e.Response, modified)
}
// XML writes an XML response.
@@ -359,10 +360,23 @@ func (e *Event) BindBody(dst any) error {
contentType := e.Request.Header.Get(headerContentType)
if strings.HasPrefix(contentType, "application/json") {
dec := json.NewDecoder(e.Request.Body)
err := dec.Decode(dst)
// note: don't use json.UnmarshalRead because it perfoms an extra
// whitespace scanning which will trigger the auto reread and
// will start again from the beginning and causing an error
//
// (technically json.UnmarshalRead is the better option here but to make
// it work it will require extra interface or other mechanism to temp disable
// the auto reread functionality and this could be a footgun for middlewares
// that wraps the body so for now it is kept as it is and it should
// be similar to the old json.NewDecoder(e.Request.Body).Decode(dst))
dec := jsontext.NewDecoder(e.Request.Body)
err := json.UnmarshalDecode(dec, dst,
// minimize breaking changes with earlier version
// @todo remove with the "Stage 2" refactoring
json.MatchCaseInsensitiveNames(true),
)
if err == nil {
// manually call Reread because single call of json.Decoder.Decode()
// manually call Reread because single call of json.UnmarshalDecode
// doesn't ensure that the entire body is a valid json string
// and it is not guaranteed that it will reach EOF to trigger the reread reset
// (ex. in case of trailing spaces or invalid trailing parts like: `{"test":1},something`)
+28 -25
View File
@@ -3,7 +3,7 @@ package router_test
import (
"bytes"
"crypto/tls"
"encoding/json"
"encoding/json/v2"
"encoding/xml"
"errors"
"fmt"
@@ -328,7 +328,7 @@ func TestEventSetAllGetAll(t *testing.T) {
"a": 123,
"b": 456,
}
rawData, err := json.Marshal(data)
rawData, err := json.Marshal(data, json.Deterministic(true))
if err != nil {
t.Fatal(err)
}
@@ -340,7 +340,7 @@ func TestEventSetAllGetAll(t *testing.T) {
data["c"] = 789
result := event.GetAll()
rawResult, err := json.Marshal(result)
rawResult, err := json.Marshal(result, json.Deterministic(true))
if err != nil {
t.Fatal(err)
}
@@ -410,8 +410,8 @@ func TestEventHTML(t *testing.T) {
func TestEventJSON(t *testing.T) {
body := map[string]any{"a": 123, "b": 456, "c": "test"}
expectedPickedBody := `{"a":123,"c":"test"}` + "\n"
expectedFullBody := `{"a":123,"b":456,"c":"test"}` + "\n"
expectedPickedBody := `{"a":123,"c":"test"}`
expectedFullBody := `{"a":123,"b":456,"c":"test"}`
scenarios := []testResponseWriteScenario[any]{
{
@@ -673,7 +673,7 @@ func TestEventFileFS(t *testing.T) {
func TestEventError(t *testing.T) {
err := new(router.Event).Error(123, "message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":123}`
if string(result) != expected {
@@ -684,7 +684,7 @@ func TestEventError(t *testing.T) {
func TestEventBadRequestError(t *testing.T) {
err := new(router.Event).BadRequestError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":400}`
if string(result) != expected {
@@ -695,7 +695,7 @@ func TestEventBadRequestError(t *testing.T) {
func TestEventNotFoundError(t *testing.T) {
err := new(router.Event).NotFoundError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":404}`
if string(result) != expected {
@@ -706,7 +706,7 @@ func TestEventNotFoundError(t *testing.T) {
func TestEventForbiddenError(t *testing.T) {
err := new(router.Event).ForbiddenError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":403}`
if string(result) != expected {
@@ -717,7 +717,7 @@ func TestEventForbiddenError(t *testing.T) {
func TestEventUnauthorizedError(t *testing.T) {
err := new(router.Event).UnauthorizedError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":401}`
if string(result) != expected {
@@ -728,7 +728,7 @@ func TestEventUnauthorizedError(t *testing.T) {
func TestEventTooManyRequestsError(t *testing.T) {
err := new(router.Event).TooManyRequestsError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":429}`
if string(result) != expected {
@@ -739,7 +739,7 @@ func TestEventTooManyRequestsError(t *testing.T) {
func TestEventInternalServerError(t *testing.T) {
err := new(router.Event).InternalServerError("message_test", map[string]any{"a": validation.Required, "b": "test"})
result, _ := json.Marshal(err)
result, _ := json.Marshal(err, json.Deterministic(true))
expected := `{"data":{"a":{"code":"validation_invalid_value","message":"Invalid value."},"b":{"code":"validation_invalid_value","message":"Invalid value."}},"message":"Message_test.","status":500}`
if string(result) != expected {
@@ -873,7 +873,7 @@ func TestEventBindBody(t *testing.T) {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
dstRaw, err := json.Marshal(dst)
dstRaw, err := json.Marshal(dst, json.Deterministic(true))
if err != nil {
t.Fatal(err)
}
@@ -925,29 +925,32 @@ func testEventResponseWrite[T any](
}
result := rec.Result()
defer result.Body.Close()
if result.StatusCode != scenario.expectedStatus {
t.Fatalf("Expected status code %d, got %d", scenario.expectedStatus, result.StatusCode)
}
resultBody, err := io.ReadAll(result.Body)
result.Body.Close()
rawBody, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
strBody := string(rawBody)
resultBody, err = json.Marshal(string(resultBody))
if err != nil {
t.Fatal(err)
// try to deserialize into a map and then serialize again in a
// deterministic manner in case it is json
var jsonBody any
err = json.Unmarshal(rawBody, &jsonBody)
if err == nil {
normalized, err := json.Marshal(jsonBody, json.Deterministic(true))
if err != nil {
t.Fatalf("Failed to deterministicly serialize json body\n%s\ngot\n%v", jsonBody, err)
}
strBody = string(normalized)
}
expectedBody, err := json.Marshal(scenario.expectedBody)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(resultBody, expectedBody) {
t.Fatalf("Expected body\n%s\ngot\n%s", expectedBody, resultBody)
if scenario.expectedBody != strBody {
t.Fatalf("Expected body\n%s\ngot\n%s", scenario.expectedBody, strBody)
}
for k, ev := range scenario.expectedHeaders {
+4 -2
View File
@@ -2,7 +2,7 @@ package router
import (
"bufio"
"encoding/json"
"encoding/json/v2"
"errors"
"io"
"log"
@@ -176,7 +176,9 @@ func ErrorHandler(resp http.ResponseWriter, req *http.Request, err error) {
resp.WriteHeader(apiErr.Status)
if req.Method != http.MethodHead {
if jsonErr := json.NewEncoder(resp).Encode(apiErr); jsonErr != nil {
// note: deterministic because some logs may depend on the exact serialized error
jsonErr := json.MarshalWrite(resp, apiErr, json.Deterministic(true))
if jsonErr != nil {
log.Println(jsonErr) // truly rare case, log to stderr only for dev purposes
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ package router
import (
"encoding"
"encoding/json"
"encoding/json/v2"
"errors"
"reflect"
"regexp"
+2 -2
View File
@@ -2,7 +2,7 @@ package router_test
import (
"bytes"
"encoding/json"
"encoding/json/v2"
"testing"
"time"
@@ -428,7 +428,7 @@ func TestUnmarshalRequestData(t *testing.T) {
return
}
raw, err := json.Marshal(s.dst)
raw, err := json.Marshal(s.dst, json.Deterministic(true))
if err != nil {
t.Fatal(err)
}