diff --git a/apis/batch.go b/apis/batch.go index 61d7bcd4..3f7367fa 100644 --- a/apis/batch.go +++ b/apis/batch.go @@ -2,7 +2,7 @@ package apis import ( "bytes" - "encoding/json" + "encoding/json/v2" "errors" "io" "mime/multipart" diff --git a/apis/collection_test.go b/apis/collection_test.go index 27051826..ff9a59ac 100644 --- a/apis/collection_test.go +++ b/apis/collection_test.go @@ -1617,7 +1617,7 @@ func TestCollectionOAuth2Providers(t *testing.T) { }, ExpectedStatus: 200, ExpectedContent: []string{ - `{"name":"oidc3","displayName":"OIDC","logo":"\u003csvg`, + `{"name":"oidc3","displayName":"OIDC","logo":" { "type": "text" }, { - "exceptDomains": null, + "exceptDomains": [], "help": "", "hidden": false, "id": "email@TEST_RANDOM", "name": "email", - "onlyDomains": null, + "onlyDomains": [], "presentable": false, "required": true, "system": true, @@ -200,7 +200,7 @@ migrate((app) => { package _test_migrations import ( - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" @@ -275,12 +275,12 @@ func init() { "type": "text" }, { - "exceptDomains": null, + "exceptDomains": [], "help": "", "hidden": false, "id": "email@TEST_RANDOM", "name": "email", - "onlyDomains": null, + "onlyDomains": [], "presentable": false, "required": true, "system": true, @@ -546,12 +546,12 @@ migrate((app) => { "type": "text" }, { - "exceptDomains": null, + "exceptDomains": [], "help": "", "hidden": false, "id": "email3885137012", "name": "email", - "onlyDomains": null, + "onlyDomains": [], "presentable": false, "required": true, "system": true, @@ -649,7 +649,7 @@ migrate((app) => { package _test_migrations import ( - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" @@ -731,12 +731,12 @@ func init() { "type": "text" }, { - "exceptDomains": null, + "exceptDomains": [], "help": "", "hidden": false, "id": "email3885137012", "name": "email", - "onlyDomains": null, + "onlyDomains": [], "presentable": false, "required": true, "system": true, @@ -1041,7 +1041,7 @@ migrate((app) => { package _test_migrations import ( - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" diff --git a/plugins/migratecmd/templates.go b/plugins/migratecmd/templates.go index 3667deb7..b135529b 100644 --- a/plugins/migratecmd/templates.go +++ b/plugins/migratecmd/templates.go @@ -2,7 +2,8 @@ package migratecmd import ( "bytes" - "encoding/json" + "encoding/json/jsontext" + "encoding/json/v2" "errors" "fmt" "path/filepath" @@ -383,7 +384,7 @@ func (p *plugin) goCreateTemplate(collection *core.Collection) (string, error) { const template = `package %s import ( - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" @@ -436,7 +437,7 @@ func (p *plugin) goDeleteTemplate(collection *core.Collection) (string, error) { const template = `package %s import ( - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" @@ -621,7 +622,7 @@ func (p *plugin) goDiffTemplate(new *core.Collection, old *core.Collection) (str if strings.Contains(combined, "json.Unmarshal(") || strings.Contains(combined, "json.Marshal(") { - imports += "\n\t\"encoding/json\"\n" + imports += "\n\t\"encoding/json/v2\"\n" } imports += "\n\t\"github.com/pocketbase/pocketbase/core\"" @@ -666,7 +667,11 @@ func init() { } func marhshalWithoutEscape(v any, prefix string, indent string) ([]byte, error) { - raw, err := json.MarshalIndent(v, prefix, indent) + raw, err := json.Marshal(v, + json.Deterministic(true), + jsontext.WithIndentPrefix(prefix), + jsontext.WithIndent(indent), + ) if err != nil { return nil, err } @@ -720,8 +725,8 @@ func diffMaps(old, new map[string]any, excludeKeys ...string) map[string]any { } // compare the serialized version of the values in case of slice or other custom type - rawOld, _ := json.Marshal(vOld) - rawNew, _ := json.Marshal(vNew) + rawOld, _ := json.Marshal(vOld, json.Deterministic(true)) + rawNew, _ := json.Marshal(vNew, json.Deterministic(true)) if !bytes.Equal(rawOld, rawNew) { // if both are maps add recursively only the changed fields diff --git a/tests/api.go b/tests/api.go index 43b820a2..499a5447 100644 --- a/tests/api.go +++ b/tests/api.go @@ -3,7 +3,7 @@ package tests import ( "bytes" "context" - "encoding/json" + "encoding/json/jsontext" "fmt" "io" "maps" @@ -259,14 +259,16 @@ func (scenario *ApiScenario) test(t testing.TB) { } } else { // normalize json response format - buffer := new(bytes.Buffer) - err := json.Compact(buffer, recorder.Body.Bytes()) var normalizedBody string + + buf := new(bytes.Buffer) + enc := jsontext.NewEncoder(buf) + err := enc.WriteValue(recorder.Body.Bytes()) if err != nil { // not a json... normalizedBody = recorder.Body.String() } else { - normalizedBody = buffer.String() + normalizedBody = buf.String() } for _, item := range scenario.ExpectedContent { diff --git a/tools/archive/create_test.go b/tools/archive/create_test.go index f75eb8ef..178a01c7 100644 --- a/tools/archive/create_test.go +++ b/tools/archive/create_test.go @@ -48,7 +48,7 @@ func TestCreateSuccess(t *testing.T) { t.Fatalf("Expected zip with name %q, got %q", zipName, name) } - expectedSize := int64(544) + expectedSize := int64(532) if size := info.Size(); size != expectedSize { t.Fatalf("Expected zip with size %d, got %d", expectedSize, size) } diff --git a/tools/auth/apple.go b/tools/auth/apple.go index 2a7da930..3a9cbb18 100644 --- a/tools/auth/apple.go +++ b/tools/auth/apple.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" diff --git a/tools/auth/auth.go b/tools/auth/auth.go index d0a9fbe8..fcac40e5 100644 --- a/tools/auth/auth.go +++ b/tools/auth/auth.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "net/http" diff --git a/tools/auth/base_provider_test.go b/tools/auth/base_provider_test.go index 1ec37012..b5bd7af4 100644 --- a/tools/auth/base_provider_test.go +++ b/tools/auth/base_provider_test.go @@ -3,7 +3,7 @@ package auth import ( "bytes" "context" - "encoding/json" + "encoding/json/v2" "testing" "golang.org/x/oauth2" @@ -215,12 +215,12 @@ func TestExtra(t *testing.T) { after := b.Extra() - rawExtra, err := json.Marshal(extra) + rawExtra, err := json.Marshal(extra, json.Deterministic(true)) if err != nil { t.Fatal(err) } - rawAfter, err := json.Marshal(after) + rawAfter, err := json.Marshal(after, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/auth/bitbucket.go b/tools/auth/bitbucket.go index 05ffa29c..fb1f39aa 100644 --- a/tools/auth/bitbucket.go +++ b/tools/auth/bitbucket.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "io" diff --git a/tools/auth/box.go b/tools/auth/box.go index b93394c5..d139ceb3 100644 --- a/tools/auth/box.go +++ b/tools/auth/box.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "fmt" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/discord.go b/tools/auth/discord.go index b3588719..acd65aaa 100644 --- a/tools/auth/discord.go +++ b/tools/auth/discord.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "fmt" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/facebook.go b/tools/auth/facebook.go index 07f61988..dd92f980 100644 --- a/tools/auth/facebook.go +++ b/tools/auth/facebook.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" @@ -53,12 +53,14 @@ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { } extracted := struct { - Id string - Name string - Email string + Id string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` Picture struct { - Data struct{ Url string } - } + Data struct { + Url string `json:"url"` + } `json:"data"` + } `json:"picture"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err diff --git a/tools/auth/gitea.go b/tools/auth/gitea.go index 12090d82..9d25f232 100644 --- a/tools/auth/gitea.go +++ b/tools/auth/gitea.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -119,9 +119,9 @@ func (p *Gitea) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) { } emails := []struct { - Email string - Verified bool - Primary bool + Email string `json:"email"` + Verified bool `json:"verified"` + Primary bool `json:"primary"` }{} if err := json.Unmarshal(content, &emails); err != nil { return "", err diff --git a/tools/auth/gitee.go b/tools/auth/gitee.go index 9bcfbeeb..45234fed 100644 --- a/tools/auth/gitee.go +++ b/tools/auth/gitee.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "io" "strconv" @@ -120,9 +120,9 @@ func (p *Gitee) fetchPrimaryEmail(token *oauth2.Token) (string, error) { } emails := []struct { - Email string - State string - Scope []string + Email string `json:"email"` + State string `json:"state"` + Scope []string `json:"scope"` }{} if err := json.Unmarshal(content, &emails); err != nil { // ignore unmarshal error in case "Keep my email address private" diff --git a/tools/auth/github.go b/tools/auth/github.go index b8d858ef..8606cc89 100644 --- a/tools/auth/github.go +++ b/tools/auth/github.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "io" "strconv" @@ -116,9 +116,9 @@ func (p *Github) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) } emails := []struct { - Email string - Verified bool - Primary bool + Email string `json:"email"` + Verified bool `json:"verified"` + Primary bool `json:"primary"` }{} if err := json.Unmarshal(content, &emails); err != nil { return "", err diff --git a/tools/auth/gitlab.go b/tools/auth/gitlab.go index c879743c..79eac0fd 100644 --- a/tools/auth/gitlab.go +++ b/tools/auth/gitlab.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "strconv" "time" diff --git a/tools/auth/google.go b/tools/auth/google.go index 4c6fe142..f989080b 100644 --- a/tools/auth/google.go +++ b/tools/auth/google.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/instagram.go b/tools/auth/instagram.go index 580149b7..43884e99 100644 --- a/tools/auth/instagram.go +++ b/tools/auth/instagram.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/internal/jwk/jwk.go b/tools/auth/internal/jwk/jwk.go index 27a1adec..0535b039 100644 --- a/tools/auth/internal/jwk/jwk.go +++ b/tools/auth/internal/jwk/jwk.go @@ -7,7 +7,7 @@ import ( "crypto/ed25519" "crypto/rsa" "encoding/base64" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -105,7 +105,7 @@ func Fetch(ctx context.Context, jwksURL string, kid string) (*JWK, error) { } jwks := struct { - Keys []*JWK + Keys []*JWK `json:"keys"` }{} err = json.Unmarshal(rawBody, &jwks) diff --git a/tools/auth/internal/jwk/jwk_test.go b/tools/auth/internal/jwk/jwk_test.go index 189efcc2..4dc43f3b 100644 --- a/tools/auth/internal/jwk/jwk_test.go +++ b/tools/auth/internal/jwk/jwk_test.go @@ -7,7 +7,7 @@ import ( "crypto/rand" "crypto/rsa" "encoding/base64" - "encoding/json" + "encoding/json/v2" "fmt" "math/big" "net/http" @@ -252,7 +252,7 @@ func TestValidateTokenSignature(t *testing.T) { } server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - _ = json.NewEncoder(res).Encode(map[string]any{"keys": []*jwk.JWK{ + _ = json.MarshalWrite(res, map[string]any{"keys": []*jwk.JWK{ { Kid: "key1", Kty: "OKP", diff --git a/tools/auth/kakao.go b/tools/auth/kakao.go index 68fd5f54..d8c143c9 100644 --- a/tools/auth/kakao.go +++ b/tools/auth/kakao.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "strconv" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/lark.go b/tools/auth/lark.go index d4d93ef9..b8bf437a 100644 --- a/tools/auth/lark.go +++ b/tools/auth/lark.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/linear.go b/tools/auth/linear.go index bf256b1a..708c2ade 100644 --- a/tools/auth/linear.go +++ b/tools/auth/linear.go @@ -3,7 +3,7 @@ package auth import ( "bytes" "context" - "encoding/json" + "encoding/json/v2" "errors" "net/http" @@ -68,7 +68,6 @@ func (p *Linear) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { } `json:"viewer"` } `json:"data"` }{} - if err := json.Unmarshal(data, &extracted); err != nil { return nil, err } diff --git a/tools/auth/livechat.go b/tools/auth/livechat.go index 0d5d480c..ffa0be71 100644 --- a/tools/auth/livechat.go +++ b/tools/auth/livechat.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/mailcow.go b/tools/auth/mailcow.go index b7c37b9b..9f39dcb0 100644 --- a/tools/auth/mailcow.go +++ b/tools/auth/mailcow.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "strings" diff --git a/tools/auth/microsoft.go b/tools/auth/microsoft.go index 29134673..47d42186 100644 --- a/tools/auth/microsoft.go +++ b/tools/auth/microsoft.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "slices" diff --git a/tools/auth/monday.go b/tools/auth/monday.go index 416f1822..3dd93370 100644 --- a/tools/auth/monday.go +++ b/tools/auth/monday.go @@ -3,7 +3,7 @@ package auth import ( "bytes" "context" - "encoding/json" + "encoding/json/v2" "errors" "net/http" diff --git a/tools/auth/notion.go b/tools/auth/notion.go index 2a0d09dd..685d0800 100644 --- a/tools/auth/notion.go +++ b/tools/auth/notion.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "net/http" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/oidc.go b/tools/auth/oidc.go index 9390eb93..e9a30a81 100644 --- a/tools/auth/oidc.go +++ b/tools/auth/oidc.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "os" diff --git a/tools/auth/patreon.go b/tools/auth/patreon.go index 3a85fc64..e3b4ca31 100644 --- a/tools/auth/patreon.go +++ b/tools/auth/patreon.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/planningcenter.go b/tools/auth/planningcenter.go index 1658f234..3ba51369 100644 --- a/tools/auth/planningcenter.go +++ b/tools/auth/planningcenter.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "github.com/pocketbase/pocketbase/tools/types" @@ -62,7 +62,7 @@ func (p *Planningcenter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { // don't map the email because users can have multiple assigned // and it's not clear if they are verified } - } + } `json:"data"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err diff --git a/tools/auth/spotify.go b/tools/auth/spotify.go index 5f4d44c5..1401cf4b 100644 --- a/tools/auth/spotify.go +++ b/tools/auth/spotify.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/strava.go b/tools/auth/strava.go index 510fb03d..3efd9074 100644 --- a/tools/auth/strava.go +++ b/tools/auth/strava.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "strconv" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/trakt.go b/tools/auth/trakt.go index 740f31bd..affa4e43 100644 --- a/tools/auth/trakt.go +++ b/tools/auth/trakt.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "net/http" "github.com/pocketbase/pocketbase/tools/types" diff --git a/tools/auth/twitch.go b/tools/auth/twitch.go index 2b336820..028ce0ae 100644 --- a/tools/auth/twitch.go +++ b/tools/auth/twitch.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "net/http" diff --git a/tools/auth/twitter.go b/tools/auth/twitter.go index b57125c7..26de646d 100644 --- a/tools/auth/twitter.go +++ b/tools/auth/twitter.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/auth/vk.go b/tools/auth/vk.go index fc75d200..1f91483e 100644 --- a/tools/auth/vk.go +++ b/tools/auth/vk.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "strconv" @@ -69,7 +69,6 @@ func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { AvatarURL string `json:"photo_max"` } `json:"response"` }{} - if err := json.Unmarshal(data, &extracted); err != nil { return nil, err } diff --git a/tools/auth/wakatime.go b/tools/auth/wakatime.go index d2fede3b..b67f66ff 100644 --- a/tools/auth/wakatime.go +++ b/tools/auth/wakatime.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" @@ -62,7 +62,6 @@ func (p *Wakatime) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { IsEmailConfirmed bool `json:"is_email_confirmed"` } `json:"data"` }{} - if err := json.Unmarshal(data, &extracted); err != nil { return nil, err } diff --git a/tools/auth/yandex.go b/tools/auth/yandex.go index 024b890d..fb02e0af 100644 --- a/tools/auth/yandex.go +++ b/tools/auth/yandex.go @@ -2,7 +2,7 @@ package auth import ( "context" - "encoding/json" + "encoding/json/v2" "github.com/pocketbase/pocketbase/tools/types" "golang.org/x/oauth2" diff --git a/tools/cron/cron_test.go b/tools/cron/cron_test.go index 9b6dc33f..e08954fd 100644 --- a/tools/cron/cron_test.go +++ b/tools/cron/cron_test.go @@ -1,7 +1,7 @@ package cron import ( - "encoding/json" + "encoding/json/v2" "slices" "sync" "testing" @@ -128,7 +128,7 @@ func TestCronAddAndRemove(t *testing.T) { "test5": `{"minutes":{"1":{}},"hours":{"2":{}},"days":{"3":{}},"months":{"4":{}},"daysOfWeek":{"5":{}}}`, } for k, v := range expectedSchedules { - raw, err := json.Marshal(indexedJobs[k].schedule) + raw, err := json.Marshal(indexedJobs[k].schedule, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/cron/job.go b/tools/cron/job.go index 8fcabd9c..7827da52 100644 --- a/tools/cron/job.go +++ b/tools/cron/job.go @@ -1,6 +1,6 @@ package cron -import "encoding/json" +import "encoding/json/v2" // Job defines a single registered cron job. type Job struct { diff --git a/tools/cron/job_test.go b/tools/cron/job_test.go index 9ce2e8ef..04fcd98e 100644 --- a/tools/cron/job_test.go +++ b/tools/cron/job_test.go @@ -1,7 +1,7 @@ package cron import ( - "encoding/json" + "encoding/json/v2" "testing" ) @@ -59,7 +59,7 @@ func TestJobMarshalJSON(t *testing.T) { j := Job{id: "test_id", schedule: s} - raw, err := json.Marshal(j) + raw, err := json.Marshal(j, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/cron/schedule_test.go b/tools/cron/schedule_test.go index edc38e11..1e7f4751 100644 --- a/tools/cron/schedule_test.go +++ b/tools/cron/schedule_test.go @@ -1,7 +1,7 @@ package cron_test import ( - "encoding/json" + "encoding/json/v2" "fmt" "testing" "time" @@ -265,7 +265,7 @@ func TestNewSchedule(t *testing.T) { return } - encoded, err := json.Marshal(schedule) + encoded, err := json.Marshal(schedule, json.Deterministic(true)) if err != nil { t.Fatalf("Failed to marshalize the result schedule: %v", err) } diff --git a/tools/dbutils/index_test.go b/tools/dbutils/index_test.go index 5346c2c2..895d35c6 100644 --- a/tools/dbutils/index_test.go +++ b/tools/dbutils/index_test.go @@ -2,7 +2,7 @@ package dbutils_test import ( "bytes" - "encoding/json" + "encoding/json/v2" "fmt" "strings" "testing" diff --git a/tools/filesystem/file_test.go b/tools/filesystem/file_test.go index b5bca2ce..3b46150f 100644 --- a/tools/filesystem/file_test.go +++ b/tools/filesystem/file_test.go @@ -64,8 +64,8 @@ func TestNewFileFromPath(t *testing.T) { if match, err := regexp.Match(normalizedNamePattern, []byte(f.Name)); !match { t.Fatalf("Expected Name to match %v, got %q (%v)", normalizedNamePattern, f.Name, err) } - if f.Size != 73 { - t.Fatalf("Expected Size %v, got %v", 73, f.Size) + if f.Size != 77 { + t.Fatalf("Expected Size %v, got %v", 77, f.Size) } if _, ok := f.Reader.(*filesystem.PathReader); !ok { t.Fatalf("Expected Reader to be PathReader, got %v", f.Reader) diff --git a/tools/filesystem/filesystem_test.go b/tools/filesystem/filesystem_test.go index 9fafdfb9..4340555f 100644 --- a/tools/filesystem/filesystem_test.go +++ b/tools/filesystem/filesystem_test.go @@ -444,7 +444,7 @@ func TestFilesystemServe(t *testing.T) { map[string]string{ "Content-Disposition": `inline; filename="test_name.png"`, "Content-Type": "image/png", - "Content-Length": "73", + "Content-Length": "77", "Content-Security-Policy": csp, "Cache-Control": cacheControl, }, @@ -459,7 +459,7 @@ func TestFilesystemServe(t *testing.T) { map[string]string{ "Content-Disposition": `attachment; filename="test_name_download.png"`, "Content-Type": "image/png", - "Content-Length": "73", + "Content-Length": "77", "Content-Security-Policy": csp, "Cache-Control": cacheControl, }, @@ -792,8 +792,8 @@ func TestFilesystemCopy(t *testing.T) { } defer f.Close() - if f.Size() != 73 { - t.Fatalf("Expected file size %d, got %d", 73, f.Size()) + if f.Size() != 77 { + t.Fatalf("Expected file size %d, got %d", 77, f.Size()) } } @@ -895,7 +895,7 @@ func TestFilesystemServeSingleRange(t *testing.T) { t.Fatalf("Expected StatusCode %d, got %d", http.StatusPartialContent, result.StatusCode) } - expectedRange := "bytes 0-20/73" + expectedRange := "bytes 0-20/77" if cr := result.Header.Get("Content-Range"); cr != expectedRange { t.Fatalf("Expected Content-Range %q, got %q", expectedRange, cr) } diff --git a/tools/filesystem/internal/fileblob/attrs.go b/tools/filesystem/internal/fileblob/attrs.go index 3f30ed3b..b5bbf8c9 100644 --- a/tools/filesystem/internal/fileblob/attrs.go +++ b/tools/filesystem/internal/fileblob/attrs.go @@ -1,7 +1,7 @@ package fileblob import ( - "encoding/json" + "encoding/json/v2" "fmt" "os" ) @@ -50,7 +50,8 @@ func setAttrs(path string, xa xattrs) error { return err } - if err := json.NewEncoder(f).Encode(xa); err != nil { + err = json.MarshalWrite(f, xa) + if err != nil { f.Close() os.Remove(f.Name()) return err @@ -75,7 +76,9 @@ func getAttrs(path string) (xattrs, error) { } xa := new(xattrs) - if err := json.NewDecoder(f).Decode(xa); err != nil { + + err = json.UnmarshalRead(f, xa) + if err != nil { f.Close() return xattrs{}, err } diff --git a/tools/filesystem/internal/s3blob/s3/error_test.go b/tools/filesystem/internal/s3blob/s3/error_test.go index 4728c391..3eb77ed0 100644 --- a/tools/filesystem/internal/s3blob/s3/error_test.go +++ b/tools/filesystem/internal/s3blob/s3/error_test.go @@ -1,7 +1,7 @@ package s3_test import ( - "encoding/json" + "encoding/json/v2" "encoding/xml" "testing" @@ -29,7 +29,7 @@ func TestResponseErrorSerialization(t *testing.T) { t.Fatal(err) } - jsonRaw, err := json.Marshal(respErr) + jsonRaw, err := json.Marshal(respErr, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/filesystem/internal/s3blob/s3/get_object_test.go b/tools/filesystem/internal/s3blob/s3/get_object_test.go index 3dde3b05..495b7f41 100644 --- a/tools/filesystem/internal/s3blob/s3/get_object_test.go +++ b/tools/filesystem/internal/s3blob/s3/get_object_test.go @@ -2,7 +2,7 @@ package s3_test import ( "context" - "encoding/json" + "encoding/json/v2" "io" "net/http" "strings" @@ -78,7 +78,7 @@ func TestS3GetObject(t *testing.T) { } // check serialized attributes - raw, err := json.Marshal(resp) + raw, err := json.Marshal(resp, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/filesystem/internal/s3blob/s3/head_object_test.go b/tools/filesystem/internal/s3blob/s3/head_object_test.go index e2bf9796..4cac81b5 100644 --- a/tools/filesystem/internal/s3blob/s3/head_object_test.go +++ b/tools/filesystem/internal/s3blob/s3/head_object_test.go @@ -2,7 +2,7 @@ package s3_test import ( "context" - "encoding/json" + "encoding/json/v2" "net/http" "testing" @@ -63,7 +63,7 @@ func TestS3HeadObject(t *testing.T) { t.Fatal(err) } - raw, err := json.Marshal(resp) + raw, err := json.Marshal(resp, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/filesystem/internal/s3blob/s3/list_objects_test.go b/tools/filesystem/internal/s3blob/s3/list_objects_test.go index cd0b7f7c..81ccd7b7 100644 --- a/tools/filesystem/internal/s3blob/s3/list_objects_test.go +++ b/tools/filesystem/internal/s3blob/s3/list_objects_test.go @@ -2,7 +2,7 @@ package s3_test import ( "context" - "encoding/json" + "encoding/json/v2" "io" "net/http" "strings" @@ -143,7 +143,7 @@ func TestS3ListObjects(t *testing.T) { t.Fatal(err) } - raw, err := json.Marshal(resp) + raw, err := json.Marshal(resp, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/filesystem/internal/s3blob/s3blob_test.go b/tools/filesystem/internal/s3blob/s3blob_test.go index 20d0f3b7..c420ab2c 100644 --- a/tools/filesystem/internal/s3blob/s3blob_test.go +++ b/tools/filesystem/internal/s3blob/s3blob_test.go @@ -2,7 +2,7 @@ package s3blob_test import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "io" @@ -261,7 +261,11 @@ func TestDriverAttributes(t *testing.T) { t.Fatal(err) } - raw, err := json.Marshal(attrs) + raw, err := json.Marshal( + attrs, + json.Deterministic(true), + json.FormatNilSliceAsNull(true), + ) if err != nil { t.Fatal(err) } @@ -362,7 +366,11 @@ func TestDriverListPaged(t *testing.T) { t.Fatal(err) } - raw, err := json.Marshal(page) + raw, err := json.Marshal( + page, + json.Deterministic(true), + json.FormatNilSliceAsNull(true), + ) if err != nil { t.Fatal(err) } @@ -510,7 +518,11 @@ func TestDriverNewRangeReader(t *testing.T) { } } - rawAttrs, err := json.Marshal(r.Attributes()) + rawAttrs, err := json.Marshal( + r.Attributes(), + json.Deterministic(true), + json.FormatNilSliceAsNull(true), + ) if err != nil { t.Fatal(err) } diff --git a/tools/list/list.go b/tools/list/list.go index 76c41eed..27c4b961 100644 --- a/tools/list/list.go +++ b/tools/list/list.go @@ -1,7 +1,7 @@ package list import ( - "encoding/json" + "encoding/json/v2" "regexp" "strings" diff --git a/tools/list/list_test.go b/tools/list/list_test.go index d169a75f..c1cd9b3a 100644 --- a/tools/list/list_test.go +++ b/tools/list/list_test.go @@ -1,7 +1,7 @@ package list_test import ( - "encoding/json" + "encoding/json/v2" "fmt" "testing" diff --git a/tools/logger/batch_handler.go b/tools/logger/batch_handler.go index 156215c0..08b22a51 100644 --- a/tools/logger/batch_handler.go +++ b/tools/logger/batch_handler.go @@ -2,7 +2,7 @@ package logger import ( "context" - "encoding/json" + "encoding/json/v2" "errors" "log/slog" "sync" diff --git a/tools/logger/batch_handler_test.go b/tools/logger/batch_handler_test.go index efb309ac..4248d03a 100644 --- a/tools/logger/batch_handler_test.go +++ b/tools/logger/batch_handler_test.go @@ -2,6 +2,7 @@ package logger import ( "context" + "encoding/json/v2" "errors" "fmt" "log/slog" @@ -326,7 +327,7 @@ func TestBatchHandlerAttrsFormat(t *testing.T) { for i, data := range expected { t.Run(fmt.Sprintf("log handler %d", i), func(t *testing.T) { log := beforeLogs[i] - raw, _ := log.Data.MarshalJSON() + raw, _ := json.Marshal(log.Data, json.Deterministic(true)) if string(raw) != data { t.Fatalf("Expected \n%s \ngot \n%s", data, raw) } diff --git a/tools/picker/pick.go b/tools/picker/pick.go index 2cd23aa5..a419a3e0 100644 --- a/tools/picker/pick.go +++ b/tools/picker/pick.go @@ -1,7 +1,7 @@ package picker import ( - "encoding/json" + "encoding/json/v2" "strings" "github.com/pocketbase/pocketbase/tools/search" diff --git a/tools/picker/pick_test.go b/tools/picker/pick_test.go index 95add588..fdd438c1 100644 --- a/tools/picker/pick_test.go +++ b/tools/picker/pick_test.go @@ -1,7 +1,7 @@ package picker_test import ( - "encoding/json" + "encoding/json/v2" "testing" "github.com/pocketbase/pocketbase/tools/picker" @@ -263,7 +263,7 @@ func TestPickFields(t *testing.T) { return } - serialized, err := json.Marshal(result) + serialized, err := json.Marshal(result, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/router/error_test.go b/tools/router/error_test.go index d6b49743..a5e397bd 100644 --- a/tools/router/error_test.go +++ b/tools/router/error_test.go @@ -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) } diff --git a/tools/router/event.go b/tools/router/event.go index 88b03786..d0f30bb5 100644 --- a/tools/router/event.go +++ b/tools/router/event.go @@ -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`) diff --git a/tools/router/event_test.go b/tools/router/event_test.go index 8b929efe..1a76cd93 100644 --- a/tools/router/event_test.go +++ b/tools/router/event_test.go @@ -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 { diff --git a/tools/router/router.go b/tools/router/router.go index c2c35ddd..77c935e8 100644 --- a/tools/router/router.go +++ b/tools/router/router.go @@ -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 } } diff --git a/tools/router/unmarshal_request_data.go b/tools/router/unmarshal_request_data.go index 099a915d..04e84738 100644 --- a/tools/router/unmarshal_request_data.go +++ b/tools/router/unmarshal_request_data.go @@ -2,7 +2,7 @@ package router import ( "encoding" - "encoding/json" + "encoding/json/v2" "errors" "reflect" "regexp" diff --git a/tools/router/unmarshal_request_data_test.go b/tools/router/unmarshal_request_data_test.go index 797719a3..94d77bb6 100644 --- a/tools/router/unmarshal_request_data_test.go +++ b/tools/router/unmarshal_request_data_test.go @@ -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) } diff --git a/tools/search/filter.go b/tools/search/filter.go index 8df74a95..48983bb8 100644 --- a/tools/search/filter.go +++ b/tools/search/filter.go @@ -1,7 +1,7 @@ package search import ( - "encoding/json" + "encoding/json/v2" "errors" "fmt" "strconv" @@ -69,7 +69,7 @@ func (f FilterData) BuildExprWithLimit( // try to json serialize as fallback if replacement == "" { - raw, _ := json.Marshal(v) + raw, _ := json.Marshal(v, json.Deterministic(true)) replacement = string(raw) } diff --git a/tools/search/multi_match_subquery_test.go b/tools/search/multi_match_subquery_test.go index 6caae968..f24564d4 100644 --- a/tools/search/multi_match_subquery_test.go +++ b/tools/search/multi_match_subquery_test.go @@ -3,7 +3,7 @@ package search_test import ( "bytes" "database/sql" - "encoding/json" + "encoding/json/v2" "testing" "github.com/pocketbase/dbx" @@ -40,7 +40,7 @@ func TestMultiMatchSubqueryBuild(t *testing.T) { } // the params from all expressions should be merged in the root - rawParams, err := json.Marshal(params) + rawParams, err := json.Marshal(params, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/search/provider_test.go b/tools/search/provider_test.go index fc73a4df..f654f0fc 100644 --- a/tools/search/provider_test.go +++ b/tools/search/provider_test.go @@ -3,7 +3,7 @@ package search import ( "context" "database/sql" - "encoding/json" + "encoding/json/v2" "errors" "fmt" "strconv" @@ -138,7 +138,7 @@ func TestProviderSort(t *testing.T) { Sort(initialSort). AddSort(SortField{"test3", SortDesc}) - encoded, _ := json.Marshal(p.sort) + encoded, _ := json.Marshal(p.sort, json.Deterministic(true)) expected := `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"},{"name":"test3","direction":"DESC"}]` if string(encoded) != expected { @@ -153,7 +153,7 @@ func TestProviderFilter(t *testing.T) { Filter(initialFilter). AddFilter("test3") - encoded, _ := json.Marshal(p.filter) + encoded, _ := json.Marshal(p.filter, json.Deterministic(true)) expected := `["test1","test2","test3"]` if string(encoded) != expected { @@ -246,12 +246,12 @@ func TestProviderParse(t *testing.T) { t.Fatalf("Expected perPage %v, got %v", s.expectPerPage, p.perPage) } - encodedSort, _ := json.Marshal(p.sort) + encodedSort, _ := json.Marshal(p.sort, json.Deterministic(true)) if string(encodedSort) != s.expectSort { t.Fatalf("Expected sort %v, got \n%v", s.expectSort, string(encodedSort)) } - encodedFilter, _ := json.Marshal(p.filter) + encodedFilter, _ := json.Marshal(p.filter, json.Deterministic(true)) if string(encodedFilter) != s.expectFilter { t.Fatalf("Expected filter %v, got \n%v", s.expectFilter, string(encodedFilter)) } @@ -453,7 +453,7 @@ func TestProviderExecNonEmptyQuery(t *testing.T) { t.Fatalf("Expected resolver.Update to be called %d, got %d", 1, testResolver.UpdateQueryCalls) } - encoded, _ := json.Marshal(result) + encoded, _ := json.Marshal(result, json.Deterministic(true)) if string(encoded) != s.expectResult { t.Fatalf("Expected result %v, got \n%v", s.expectResult, string(encoded)) } @@ -722,7 +722,7 @@ func TestProviderParseAndExec(t *testing.T) { t.Fatalf("Expected %d db queries, got %d: \n%v", expectedQueries, len(testDB.CalledQueries), testDB.CalledQueries) } - encoded, _ := json.Marshal(result) + encoded, _ := json.Marshal(result, json.Deterministic(true)) if string(encoded) != s.expectResult { t.Fatalf("Expected result \n%v\ngot\n%v", s.expectResult, string(encoded)) } diff --git a/tools/search/sort_test.go b/tools/search/sort_test.go index 1c23cc45..e47ad6cb 100644 --- a/tools/search/sort_test.go +++ b/tools/search/sort_test.go @@ -1,7 +1,7 @@ package search_test import ( - "encoding/json" + "encoding/json/v2" "fmt" "testing" @@ -67,7 +67,7 @@ func TestParseSortFromString(t *testing.T) { for _, s := range scenarios { t.Run(s.value, func(t *testing.T) { result := search.ParseSortFromString(s.value) - encoded, _ := json.Marshal(result) + encoded, _ := json.Marshal(result, json.Deterministic(true)) encodedStr := string(encoded) if encodedStr != s.expected { diff --git a/tools/store/store.go b/tools/store/store.go index bdb5d96a..087a6c39 100644 --- a/tools/store/store.go +++ b/tools/store/store.go @@ -1,7 +1,7 @@ package store import ( - "encoding/json" + "encoding/json/v2" "sync" ) diff --git a/tools/store/store_test.go b/tools/store/store_test.go index 24f28741..8d9b88de 100644 --- a/tools/store/store_test.go +++ b/tools/store/store_test.go @@ -2,7 +2,7 @@ package store_test import ( "bytes" - "encoding/json" + "encoding/json/v2" "slices" "strconv" "testing" @@ -12,7 +12,7 @@ import ( func TestNew(t *testing.T) { data := map[string]int{"test1": 1, "test2": 2} - originalRawData, err := json.Marshal(data) + originalRawData, err := json.Marshal(data, json.Deterministic(true)) if err != nil { t.Fatal(err) } @@ -22,7 +22,7 @@ func TestNew(t *testing.T) { s.Remove("test1") // remove 1 item // check if data was shallow copied - rawData, _ := json.Marshal(data) + rawData, _ := json.Marshal(data, json.Deterministic(true)) if !bytes.Equal(originalRawData, rawData) { t.Fatalf("Expected data \n%s, \ngot \n%s", originalRawData, rawData) } @@ -44,7 +44,7 @@ func TestReset(t *testing.T) { s := store.New(map[string]int{"test1": 1}) data := map[string]int{"test2": 2} - originalRawData, err := json.Marshal(data) + originalRawData, err := json.Marshal(data, json.Deterministic(true)) if err != nil { t.Fatal(err) } @@ -53,7 +53,7 @@ func TestReset(t *testing.T) { s.Set("test3", 3) // check if data was shallow copied - rawData, _ := json.Marshal(data) + rawData, _ := json.Marshal(data, json.Deterministic(true)) if !bytes.Equal(originalRawData, rawData) { t.Fatalf("Expected data \n%s, \ngot \n%s", originalRawData, rawData) } @@ -371,7 +371,7 @@ func TestMarshalJSON(t *testing.T) { expected := []byte(`{"a":"test1", "b":"test2"}`) - result, err := json.Marshal(s) + result, err := json.Marshal(s, json.Deterministic(true)) if err != nil { t.Fatal(err) } diff --git a/tools/subscriptions/client.go b/tools/subscriptions/client.go index 06309d54..3bff8018 100644 --- a/tools/subscriptions/client.go +++ b/tools/subscriptions/client.go @@ -1,7 +1,7 @@ package subscriptions import ( - "encoding/json" + "encoding/json/v2" "net/url" "strings" "sync" diff --git a/tools/subscriptions/client_test.go b/tools/subscriptions/client_test.go index ff5b59b7..eec6b817 100644 --- a/tools/subscriptions/client_test.go +++ b/tools/subscriptions/client_test.go @@ -1,7 +1,7 @@ package subscriptions_test import ( - "encoding/json" + "encoding/json/v2" "strings" "sync" "testing" diff --git a/tools/types/datetime.go b/tools/types/datetime.go index 26820391..b302e994 100644 --- a/tools/types/datetime.go +++ b/tools/types/datetime.go @@ -2,7 +2,7 @@ package types import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "time" "github.com/spf13/cast" diff --git a/tools/types/geo_point.go b/tools/types/geo_point.go index 286120d0..bbe3a7b9 100644 --- a/tools/types/geo_point.go +++ b/tools/types/geo_point.go @@ -2,7 +2,7 @@ package types import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "fmt" ) diff --git a/tools/types/json_array.go b/tools/types/json_array.go index 024fd7b0..a7749a9f 100644 --- a/tools/types/json_array.go +++ b/tools/types/json_array.go @@ -2,7 +2,7 @@ package types import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "fmt" ) @@ -14,12 +14,9 @@ type jsonArrayAlias[T any] JSONArray[T] // MarshalJSON implements the [json.Marshaler] interface. func (m JSONArray[T]) MarshalJSON() ([]byte, error) { - // initialize an empty map to ensure that `[]` is returned as json - if m == nil { - m = JSONArray[T]{} - } - - return json.Marshal(jsonArrayAlias[T](m)) + // note: forces the Deterministic option to ensure consistent output + // in mixed json v1 and v2 configurations + return json.Marshal(jsonArrayAlias[T](m), json.Deterministic(true)) } // String returns the string representation of the current json array. @@ -30,8 +27,7 @@ func (m JSONArray[T]) String() string { // Value implements the [driver.Valuer] interface. func (m JSONArray[T]) Value() (driver.Value, error) { - data, err := json.Marshal(m) - + data, err := m.MarshalJSON() return string(data), err } @@ -54,5 +50,12 @@ func (m *JSONArray[T]) Scan(value any) error { data = []byte("[]") } - return json.Unmarshal(data, m) + err := json.Unmarshal(data, m) + if err != nil { + // reset because jsonv2 performs streaming decoding and mutates the dst even on error + *m = JSONArray[T]{} + return err + } + + return nil } diff --git a/tools/types/json_array_test.go b/tools/types/json_array_test.go index 27aa16fe..8527e083 100644 --- a/tools/types/json_array_test.go +++ b/tools/types/json_array_test.go @@ -2,7 +2,7 @@ package types_test import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "fmt" "testing" @@ -115,7 +115,7 @@ func TestJSONArrayScan(t *testing.T) { continue } - result, _ := arr.MarshalJSON() + result, _ := json.Marshal(arr, json.Deterministic(true)) if string(result) != s.expectJSON { t.Errorf("(%d) Expected %s, got %v", i, s.expectJSON, string(result)) diff --git a/tools/types/json_map.go b/tools/types/json_map.go index 87b0fc84..f51eeeaa 100644 --- a/tools/types/json_map.go +++ b/tools/types/json_map.go @@ -2,31 +2,13 @@ package types import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "fmt" ) // JSONMap defines a map that is safe for json and db read/write. type JSONMap[T any] map[string]T -// MarshalJSON implements the [json.Marshaler] interface. -func (m JSONMap[T]) MarshalJSON() ([]byte, error) { - type alias JSONMap[T] // prevent recursion - - // initialize an empty map to ensure that `{}` is returned as json - if m == nil { - m = JSONMap[T]{} - } - - return json.Marshal(alias(m)) -} - -// String returns the string representation of the current json map. -func (m JSONMap[T]) String() string { - v, _ := m.MarshalJSON() - return string(v) -} - // Get retrieves a single value from the current JSONMap[T]. // // This helper was added primarily to assist the goja integration since custom map types @@ -43,10 +25,24 @@ func (m JSONMap[T]) Set(key string, value T) { m[key] = value } +// MarshalJSON implements the [json.Marshaler] interface. +func (m JSONMap[T]) MarshalJSON() ([]byte, error) { + type alias JSONMap[T] // prevent recursion + + // note: forces the Deterministic option to ensure consistent output + // in mixed json v1 and v2 configurations + return json.Marshal(alias(m), json.Deterministic(true)) +} + +// String returns the string representation of the current json map. +func (m JSONMap[T]) String() string { + v, _ := m.MarshalJSON() + return string(v) +} + // Value implements the [driver.Valuer] interface. func (m JSONMap[T]) Value() (driver.Value, error) { - data, err := json.Marshal(m) - + data, err := m.MarshalJSON() return string(data), err } @@ -69,5 +65,12 @@ func (m *JSONMap[T]) Scan(value any) error { data = []byte("{}") } - return json.Unmarshal(data, m) + err := json.Unmarshal(data, m) + if err != nil { + // reset because jsonv2 performs streaming decoding and mutates the dst even on error + *m = JSONMap[T]{} + return err + } + + return nil } diff --git a/tools/types/json_map_test.go b/tools/types/json_map_test.go index 56ea95aa..eab4b949 100644 --- a/tools/types/json_map_test.go +++ b/tools/types/json_map_test.go @@ -2,6 +2,7 @@ package types_test import ( "database/sql/driver" + "encoding/json/v2" "fmt" "testing" @@ -154,7 +155,7 @@ func TestJSONArrayMapScan(t *testing.T) { t.Fatalf("Expected %v, got %v (%v)", s.expectError, hasErr, scanErr) } - result, _ := arr.MarshalJSON() + result, _ := json.Marshal(arr, json.Deterministic(true)) if string(result) != s.expectJSON { t.Fatalf("Expected %s, got %s", s.expectJSON, result) diff --git a/tools/types/json_raw.go b/tools/types/json_raw.go index 8ed58c2a..2eb9cdaa 100644 --- a/tools/types/json_raw.go +++ b/tools/types/json_raw.go @@ -2,7 +2,7 @@ package types import ( "database/sql/driver" - "encoding/json" + "encoding/json/v2" "errors" )