Skip to content

Commit 151ef80

Browse files
junoberryferrytechknowlogickwxiaoguang
authored
use experimental go json v2 library (#35392)
details: https://pkg.go.dev/encoding/json/v2 --------- Co-authored-by: techknowlogick <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 8106d95 commit 151ef80

File tree

19 files changed

+240
-37
lines changed

19 files changed

+240
-37
lines changed

.github/workflows/pull-db-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ jobs:
7272
go-version-file: go.mod
7373
check-latest: true
7474
- run: make deps-backend
75-
- run: make backend
75+
- run: GOEXPERIMENT='' make backend
7676
env:
7777
TAGS: bindata gogit sqlite sqlite_unlock_notify
7878
- name: run migration tests
7979
run: make test-sqlite-migration
8080
- name: run tests
81-
run: make test-sqlite
81+
run: GOEXPERIMENT='' make test-sqlite
8282
timeout-minutes: 50
8383
env:
8484
TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -142,7 +142,7 @@ jobs:
142142
RACE_ENABLED: true
143143
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
144144
- name: unit-tests-gogit
145-
run: make unit-test-coverage test-check
145+
run: GOEXPERIMENT='' make unit-test-coverage test-check
146146
env:
147147
TAGS: bindata gogit
148148
RACE_ENABLED: true

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ DIST := dist
1818
DIST_DIRS := $(DIST)/binaries $(DIST)/release
1919
IMPORT := code.gitea.io/gitea
2020

21+
# By default use go's 1.25 experimental json v2 library when building
22+
# TODO: remove when no longer experimental
23+
export GOEXPERIMENT ?= jsonv2
24+
2125
GO ?= go
2226
SHASUM ?= shasum -a 256
2327
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
@@ -766,7 +770,7 @@ generate-go: $(TAGS_PREREQ)
766770

767771
.PHONY: security-check
768772
security-check:
769-
go run $(GOVULNCHECK_PACKAGE) -show color ./...
773+
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
770774

771775
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
772776
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ require (
277277
go.uber.org/zap v1.27.0 // indirect
278278
go.uber.org/zap/exp v0.3.0 // indirect
279279
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
280-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
280+
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
281281
golang.org/x/mod v0.27.0 // indirect
282282
golang.org/x/time v0.12.0 // indirect
283283
golang.org/x/tools v0.36.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,8 +848,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
848848
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
849849
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
850850
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
851-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
852-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
851+
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
852+
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
853853
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
854854
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
855855
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=

modules/assetfs/embed.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error {
365365
if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
366366
return err
367367
}
368-
jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
368+
jsonBuf, err := json.Marshal(meta)
369369
if err != nil {
370370
return err
371371
}
372372
_, _ = output.Write([]byte{'\n'})
373-
_, err = output.Write(jsonBuf)
373+
_, err = output.Write(bytes.TrimSpace(jsonBuf))
374374
return err
375375
}

modules/json/json.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ type Interface interface {
3232
}
3333

3434
var (
35-
// DefaultJSONHandler default json handler
36-
DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
35+
DefaultJSONHandler = getDefaultJSONHandler()
3736

3837
_ Interface = StdJSON{}
3938
_ Interface = JSONiter{}

modules/json/json_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package json
55

66
import (
7+
"bytes"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
@@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) {
1617
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
1718
assert.NoError(t, err)
1819
}
20+
21+
func TestIndent(t *testing.T) {
22+
buf := &bytes.Buffer{}
23+
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
24+
assert.NoError(t, err)
25+
assert.Equal(t, `{
26+
> "a": 1
27+
>}`, buf.String())
28+
}

modules/json/jsonlegacy.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build !goexperiment.jsonv2
5+
6+
package json
7+
8+
import (
9+
"io"
10+
11+
jsoniter "github.com/json-iterator/go"
12+
)
13+
14+
func getDefaultJSONHandler() Interface {
15+
return JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
16+
}
17+
18+
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
19+
return DefaultJSONHandler.Marshal(v)
20+
}
21+
22+
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
23+
return DefaultJSONHandler.NewDecoder(reader)
24+
}

modules/json/jsonv2.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
//go:build goexperiment.jsonv2
5+
6+
package json
7+
8+
import (
9+
"bytes"
10+
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
11+
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
12+
"io"
13+
)
14+
15+
// JSONv2 implements Interface via encoding/json/v2
16+
// Requires GOEXPERIMENT=jsonv2 to be set at build time
17+
type JSONv2 struct {
18+
marshalOptions jsonv2.Options
19+
marshalKeepOptionalEmptyOptions jsonv2.Options
20+
unmarshalOptions jsonv2.Options
21+
unmarshalCaseInsensitiveOptions jsonv2.Options
22+
}
23+
24+
var jsonV2 JSONv2
25+
26+
func init() {
27+
commonMarshalOptions := []jsonv2.Options{
28+
jsonv2.FormatNilSliceAsNull(true),
29+
jsonv2.FormatNilMapAsNull(true),
30+
}
31+
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
32+
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
33+
34+
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
35+
// v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept.
36+
// Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2
37+
jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...)
38+
39+
// Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig)
40+
jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true))
41+
}
42+
43+
func getDefaultJSONHandler() Interface {
44+
return &jsonV2
45+
}
46+
47+
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
48+
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
49+
}
50+
51+
func (j *JSONv2) Marshal(v any) ([]byte, error) {
52+
return jsonv2.Marshal(v, j.marshalOptions)
53+
}
54+
55+
func (j *JSONv2) Unmarshal(data []byte, v any) error {
56+
return jsonv2.Unmarshal(data, v, j.unmarshalOptions)
57+
}
58+
59+
func (j *JSONv2) NewEncoder(writer io.Writer) Encoder {
60+
return &jsonV2Encoder{writer: writer, opts: j.marshalOptions}
61+
}
62+
63+
func (j *JSONv2) NewDecoder(reader io.Reader) Decoder {
64+
return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions}
65+
}
66+
67+
// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet)
68+
func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
69+
return jsonv1.Indent(dst, src, prefix, indent)
70+
}
71+
72+
type jsonV2Encoder struct {
73+
writer io.Writer
74+
opts jsonv2.Options
75+
}
76+
77+
func (e *jsonV2Encoder) Encode(v any) error {
78+
return jsonv2.MarshalWrite(e.writer, v, e.opts)
79+
}
80+
81+
type jsonV2Decoder struct {
82+
reader io.Reader
83+
opts jsonv2.Options
84+
}
85+
86+
func (d *jsonV2Decoder) Decode(v any) error {
87+
return jsonv2.UnmarshalRead(d.reader, v, d.opts)
88+
}
89+
90+
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
91+
return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions}
92+
}

modules/lfs/http_client_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
193193
},
194194
{
195195
endpoint: "https://invalid-json-response.io",
196-
expectedError: "invalid json",
196+
expectedError: "/(invalid json|jsontext: invalid character)/",
197197
},
198198
{
199199
endpoint: "https://valid-batch-request-download.io",
@@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) {
258258
return nil
259259
})
260260
if c.expectedError != "" {
261-
assert.ErrorContains(t, err, c.expectedError)
261+
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
262+
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
263+
} else {
264+
assert.ErrorContains(t, err, c.expectedError)
265+
}
262266
} else {
263267
assert.NoError(t, err)
264268
}
@@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
297301
},
298302
{
299303
endpoint: "https://invalid-json-response.io",
300-
expectedError: "invalid json",
304+
expectedError: "/(invalid json|jsontext: invalid character)/",
301305
},
302306
{
303307
endpoint: "https://valid-batch-request-upload.io",
@@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) {
352356
return io.NopCloser(new(bytes.Buffer)), objectError
353357
})
354358
if c.expectedError != "" {
355-
assert.ErrorContains(t, err, c.expectedError)
359+
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
360+
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
361+
} else {
362+
assert.ErrorContains(t, err, c.expectedError)
363+
}
356364
} else {
357365
assert.NoError(t, err)
358366
}

0 commit comments

Comments
 (0)