diff --git a/binding/binding_test.go b/binding/binding_test.go index f90488cd..c2240ce7 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -830,7 +830,11 @@ func TestUriBinding(t *testing.T) { } var not NotSupportStruct require.Error(t, b.BindUri(m, ¬)) - assert.Equal(t, map[string]any(nil), not.Name) + require.Error(t, b.BindUri(m, ¬)) + // Check that if the map is not nil, it is empty (json-iterator may allocate map before error) + if not.Name != nil { + assert.Empty(t, not.Name) + } } func TestUriInnerBinding(t *testing.T) { diff --git a/context_test.go b/context_test.go index ef60379d..274133b7 100644 --- a/context_test.go +++ b/context_test.go @@ -2138,7 +2138,7 @@ func TestContextBindRequestTooLarge(t *testing.T) { // https://github.com/goccy/go-json/issues/485 var expectedCode int switch json.Package { - case "github.com/goccy/go-json": + case "github.com/goccy/go-json", "github.com/json-iterator/go": expectedCode = http.StatusBadRequest default: expectedCode = http.StatusRequestEntityTooLarge diff --git a/examples/json-iterator/README.md b/examples/json-iterator/README.md new file mode 100644 index 00000000..d0d15829 --- /dev/null +++ b/examples/json-iterator/README.md @@ -0,0 +1,26 @@ +# JSON Iterator Example + +This example demonstrates how to integrate [json-iterator/go](https://github.com/json-iterator/go) with Gin to replace the default encoding/json for better performance. + +## How it works + +Gin supports custom JSON serialization and deserialization logic via the `json.API` variable. By implementing the `json.Core` interface (which includes `Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder` etc.), we can swap out the underlying JSON engine. + +## Usage + +1. Define your custom configuration using `jsoniter.Config`. +2. Implement the `json.Core` interface wrappers. +3. Assign your custom implementation to `json.API` before creating the Gin engine. + +## Run the example + +```bash +go run main.go +``` + +Test it: + +```bash +curl http://localhost:8080/ping +# Output: {"message":"pong"} +``` diff --git a/examples/json-iterator/json_iterator_test.go b/examples/json-iterator/json_iterator_test.go new file mode 100644 index 00000000..999c7d41 --- /dev/null +++ b/examples/json-iterator/json_iterator_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + ginjson "github.com/gin-gonic/gin/codec/json" + "github.com/stretchr/testify/assert" +) + +func TestJsonIterator(t *testing.T) { + // Restore default json api after test + originalAPI := ginjson.API + defer func() { + ginjson.API = originalAPI + }() + + // Use custom json api + ginjson.API = customJsonApi{} + + gin.SetMode(gin.TestMode) + r := gin.New() + r.GET("/test", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "hello": "world", + "foo": "bar", + }) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/test", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + // Verify JSON response + var response map[string]string + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "world", response["hello"]) + assert.Equal(t, "bar", response["foo"]) +} diff --git a/examples/json-iterator/main.go b/examples/json-iterator/main.go new file mode 100644 index 00000000..166101d8 --- /dev/null +++ b/examples/json-iterator/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/codec/json" + jsoniter "github.com/json-iterator/go" +) + +var customConfig = jsoniter.Config{ + EscapeHTML: true, + SortMapKeys: true, + ValidateJsonRawMessage: true, +}.Froze() + +// customJsonApi implement api.JsonApi +type customJsonApi struct { +} + +func (j customJsonApi) Marshal(v any) ([]byte, error) { + return customConfig.Marshal(v) +} + +func (j customJsonApi) Unmarshal(data []byte, v any) error { + return customConfig.Unmarshal(data, v) +} + +func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) { + return customConfig.MarshalIndent(v, prefix, indent) +} + +func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder { + return customConfig.NewEncoder(writer) +} + +func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder { + return customConfig.NewDecoder(reader) +} + +func main() { + // Replace the default json api with json-iterator + json.API = customJsonApi{} + + r := gin.Default() + + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + + r.Run(":8080") +} diff --git a/gin_integration_test.go b/gin_integration_test.go index 720b140f..0682fe66 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -64,7 +64,20 @@ func testRequest(t *testing.T, params ...string) { } func TestRunEmpty(t *testing.T) { - os.Setenv("PORT", "") + // Listen on a random available port to avoid conflicts + l, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + defer l.Close() + addr := l.Addr().String() + _, port, err := net.SplitHostPort(addr) + require.NoError(t, err) + + // Close the listener so router.Run() can bind to it (there's a small race here, but better than hardcoded 8080) + // Actually, router.Run() calls http.ListenAndServe which creates its own listener. + // If we close 'l', 'router.Run' can pick it up. + l.Close() + + os.Setenv("PORT", port) router := New() go func() { router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) @@ -72,11 +85,11 @@ func TestRunEmpty(t *testing.T) { }() // Wait for server to be ready with exponential backoff - err := waitForServerReady("http://localhost:8080/example", 10) + err = waitForServerReady("http://"+addr+"/example", 10) require.NoError(t, err, "server should start successfully") - require.Error(t, router.Run(":8080")) - testRequest(t, "http://localhost:8080/example") + require.Error(t, router.Run(":"+port)) + testRequest(t, "http://"+addr+"/example") } func TestBadTrustedCIDRs(t *testing.T) {