Skip to content

Commit d5518e9

Browse files
committed
Implement experimental marshaler for OpenAPI V3
1 parent 3c0fae5 commit d5518e9

18 files changed

+483
-2
lines changed

pkg/internal/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ var UseOptimizedJSONUnmarshalingV3 bool = true
2222

2323
// Used by tests to selectively disable experimental JSON marshaler
2424
var UseOptimizedJSONMarshaling bool = true
25+
var UseOptimizedJSONMarshalingV3 bool = true

pkg/spec3/benchmark_serialization_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/google/go-cmp/cmp"
1212
fuzz "github.com/google/gofuzz"
1313
"k8s.io/kube-openapi/pkg/internal"
14+
jsontesting "k8s.io/kube-openapi/pkg/util/jsontesting"
1415
"k8s.io/kube-openapi/pkg/validation/spec"
1516
)
1617

@@ -71,6 +72,95 @@ func TestOpenAPIV3Deserialize(t *testing.T) {
7172
}
7273
}
7374

75+
func TestOpenAPIV3Serialize(t *testing.T) {
76+
swagFile, err := os.Open("./testdata/appsv1spec.json")
77+
if err != nil {
78+
t.Fatal(err)
79+
}
80+
defer swagFile.Close()
81+
originalJSON, err := io.ReadAll(swagFile)
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
var openapi *OpenAPI
86+
if err := json.Unmarshal(originalJSON, &openapi); err != nil {
87+
t.Fatal(err)
88+
}
89+
90+
internal.UseOptimizedJSONUnmarshalingV3 = false
91+
want, err := json.Marshal(openapi)
92+
if err != nil {
93+
t.Fatal(err)
94+
}
95+
internal.UseOptimizedJSONUnmarshalingV3 = true
96+
got, err := openapi.MarshalJSON()
97+
if err != nil {
98+
t.Fatal(err)
99+
}
100+
if err := jsontesting.JsonCompare(want, got); err != nil {
101+
t.Errorf("marshal doesn't match: %v", err)
102+
}
103+
}
104+
105+
func TestOpenAPIV3SerializeFuzzed(t *testing.T) {
106+
var fuzzer *fuzz.Fuzzer
107+
fuzzer = fuzz.NewWithSeed(1646791953)
108+
fuzzer.MaxDepth(13).NilChance(0.075).NumElements(1, 2)
109+
fuzzer.Funcs(OpenAPIV3FuzzFuncs...)
110+
111+
for i := 0; i < 100; i++ {
112+
openapi := &OpenAPI{}
113+
fuzzer.Fuzz(openapi)
114+
115+
internal.UseOptimizedJSONUnmarshalingV3 = false
116+
want, err := json.Marshal(openapi)
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
internal.UseOptimizedJSONUnmarshalingV3 = true
121+
got, err := openapi.MarshalJSON()
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
if err := jsontesting.JsonCompare(want, got); err != nil {
126+
t.Errorf("fuzzed marshal doesn't match: %v", err)
127+
}
128+
}
129+
}
130+
131+
func TestOpenAPIV3SerializeStable(t *testing.T) {
132+
swagFile, err := os.Open("./testdata/appsv1spec.json")
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
defer swagFile.Close()
137+
originalJSON, err := io.ReadAll(swagFile)
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
var openapi *OpenAPI
142+
if err := json.Unmarshal(originalJSON, &openapi); err != nil {
143+
t.Fatal(err)
144+
}
145+
146+
internal.UseOptimizedJSONUnmarshalingV3 = true
147+
for i := 0; i < 5; i++ {
148+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
149+
want, err := openapi.MarshalJSON()
150+
if err != nil {
151+
t.Fatal(err)
152+
}
153+
got, err := openapi.MarshalJSON()
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
if err := jsontesting.JsonCompare(want, got); err != nil {
158+
t.Errorf("marshal doesn't match: %v", err)
159+
}
160+
})
161+
}
162+
}
163+
74164
func BenchmarkOpenAPIV3Deserialize(b *testing.B) {
75165
benchcases := []struct {
76166
file string
@@ -142,3 +232,61 @@ func BenchmarkOpenAPIV3Deserialize(b *testing.B) {
142232
})
143233
}
144234
}
235+
236+
func BenchmarkOpenAPIV3Serialize(b *testing.B) {
237+
benchcases := []struct {
238+
file string
239+
}{
240+
{
241+
file: "appsv1spec.json",
242+
},
243+
{
244+
file: "authorizationv1spec.json",
245+
},
246+
}
247+
for _, bc := range benchcases {
248+
swagFile, err := os.Open("./testdata/" + bc.file)
249+
if err != nil {
250+
b.Fatal(err)
251+
}
252+
defer swagFile.Close()
253+
originalJSON, err := io.ReadAll(swagFile)
254+
if err != nil {
255+
b.Fatal(err)
256+
}
257+
var openapi *OpenAPI
258+
if err := json.Unmarshal(originalJSON, &openapi); err != nil {
259+
b.Fatal(err)
260+
}
261+
b.ResetTimer()
262+
b.Run(fmt.Sprintf("%s jsonv1", bc.file), func(b2 *testing.B) {
263+
b2.ReportAllocs()
264+
internal.UseOptimizedJSONMarshalingV3 = false
265+
for i := 0; i < b2.N; i++ {
266+
if _, err := json.Marshal(openapi); err != nil {
267+
b2.Fatal(err)
268+
}
269+
}
270+
})
271+
272+
b.Run(fmt.Sprintf("%s jsonv2 via jsonv1 full spec", bc.file), func(b2 *testing.B) {
273+
b2.ReportAllocs()
274+
internal.UseOptimizedJSONMarshalingV3 = true
275+
for i := 0; i < b2.N; i++ {
276+
if _, err := json.Marshal(openapi); err != nil {
277+
b2.Fatal(err)
278+
}
279+
}
280+
})
281+
282+
b.Run("jsonv2", func(b2 *testing.B) {
283+
b2.ReportAllocs()
284+
internal.UseOptimizedJSONMarshalingV3 = true
285+
for i := 0; i < b2.N; i++ {
286+
if _, err := openapi.MarshalJSON(); err != nil {
287+
b2.Fatal(err)
288+
}
289+
}
290+
})
291+
}
292+
}

pkg/spec3/encoding.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ type Encoding struct {
3232

3333
// MarshalJSON is a custom marshal function that knows how to encode Encoding as JSON
3434
func (e *Encoding) MarshalJSON() ([]byte, error) {
35+
if internal.UseOptimizedJSONMarshalingV3 {
36+
return internal.DeterministicMarshal(e)
37+
}
3538
b1, err := json.Marshal(e.EncodingProps)
3639
if err != nil {
3740
return nil, err
@@ -43,6 +46,16 @@ func (e *Encoding) MarshalJSON() ([]byte, error) {
4346
return swag.ConcatJSON(b1, b2), nil
4447
}
4548

49+
func (e *Encoding) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error {
50+
var x struct {
51+
EncodingProps encodingPropsOmitZero `json:",inline"`
52+
spec.Extensions
53+
}
54+
x.Extensions = internal.SanitizeExtensions(e.Extensions)
55+
x.EncodingProps = encodingPropsOmitZero(e.EncodingProps)
56+
return opts.MarshalNext(enc, x)
57+
}
58+
4659
func (e *Encoding) UnmarshalJSON(data []byte) error {
4760
if internal.UseOptimizedJSONUnmarshalingV3 {
4861
return jsonv2.Unmarshal(data, e)
@@ -82,3 +95,11 @@ type EncodingProps struct {
8295
// AllowReserved determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986
8396
AllowReserved bool `json:"allowReserved,omitempty"`
8497
}
98+
99+
type encodingPropsOmitZero struct {
100+
ContentType string `json:"contentType,omitempty"`
101+
Headers map[string]*Header `json:"headers,omitempty"`
102+
Style string `json:"style,omitempty"`
103+
Explode bool `json:"explode,omitzero"`
104+
AllowReserved bool `json:"allowReserved,omitzero"`
105+
}

pkg/spec3/example.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ type Example struct {
3636

3737
// MarshalJSON is a custom marshal function that knows how to encode RequestBody as JSON
3838
func (e *Example) MarshalJSON() ([]byte, error) {
39+
if internal.UseOptimizedJSONMarshalingV3 {
40+
return internal.DeterministicMarshal(e)
41+
}
3942
b1, err := json.Marshal(e.Refable)
4043
if err != nil {
4144
return nil, err
@@ -50,6 +53,17 @@ func (e *Example) MarshalJSON() ([]byte, error) {
5053
}
5154
return swag.ConcatJSON(b1, b2, b3), nil
5255
}
56+
func (e *Example) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error {
57+
var x struct {
58+
Ref string `json:"$ref,omitempty"`
59+
ExampleProps `json:",inline"`
60+
spec.Extensions
61+
}
62+
x.Ref = e.Refable.Ref.String()
63+
x.Extensions = internal.SanitizeExtensions(e.Extensions)
64+
x.ExampleProps = e.ExampleProps
65+
return opts.MarshalNext(enc, x)
66+
}
5367

5468
func (e *Example) UnmarshalJSON(data []byte) error {
5569
if internal.UseOptimizedJSONUnmarshalingV3 {

pkg/spec3/external_documentation.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type ExternalDocumentationProps struct {
3939

4040
// MarshalJSON is a custom marshal function that knows how to encode Responses as JSON
4141
func (e *ExternalDocumentation) MarshalJSON() ([]byte, error) {
42+
if internal.UseOptimizedJSONMarshalingV3 {
43+
return internal.DeterministicMarshal(e)
44+
}
4245
b1, err := json.Marshal(e.ExternalDocumentationProps)
4346
if err != nil {
4447
return nil, err
@@ -50,6 +53,16 @@ func (e *ExternalDocumentation) MarshalJSON() ([]byte, error) {
5053
return swag.ConcatJSON(b1, b2), nil
5154
}
5255

56+
func (e *ExternalDocumentation) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error {
57+
var x struct {
58+
ExternalDocumentationProps `json:",inline"`
59+
spec.Extensions
60+
}
61+
x.Extensions = internal.SanitizeExtensions(e.Extensions)
62+
x.ExternalDocumentationProps = e.ExternalDocumentationProps
63+
return opts.MarshalNext(enc, x)
64+
}
65+
5366
func (e *ExternalDocumentation) UnmarshalJSON(data []byte) error {
5467
if internal.UseOptimizedJSONUnmarshalingV3 {
5568
return jsonv2.Unmarshal(data, e)

pkg/spec3/fuzz.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,21 @@ var OpenAPIV3FuzzFuncs []interface{} = []interface{}{
169169
c.Fuzz(&v.ResponseProps)
170170
c.Fuzz(&v.VendorExtensible)
171171
},
172+
func(v *Operation, c fuzz.Continue) {
173+
c.FuzzNoCustom(v)
174+
// Do not fuzz null values into the array.
175+
for i, val := range v.SecurityRequirement {
176+
if val == nil {
177+
v.SecurityRequirement[i] = make(map[string][]string)
178+
}
179+
180+
for k, v := range val {
181+
if v == nil {
182+
val[k] = make([]string, 0)
183+
}
184+
}
185+
}
186+
},
172187
func(v *spec.Extensions, c fuzz.Continue) {
173188
numChildren := c.Intn(5)
174189
for i := 0; i < numChildren; i++ {

pkg/spec3/header.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ type Header struct {
3636

3737
// MarshalJSON is a custom marshal function that knows how to encode Header as JSON
3838
func (h *Header) MarshalJSON() ([]byte, error) {
39+
if internal.UseOptimizedJSONMarshalingV3 {
40+
return internal.DeterministicMarshal(h)
41+
}
3942
b1, err := json.Marshal(h.Refable)
4043
if err != nil {
4144
return nil, err
@@ -51,6 +54,18 @@ func (h *Header) MarshalJSON() ([]byte, error) {
5154
return swag.ConcatJSON(b1, b2, b3), nil
5255
}
5356

57+
func (h *Header) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error {
58+
var x struct {
59+
Ref string `json:"$ref,omitempty"`
60+
HeaderProps headerPropsOmitZero `json:",inline"`
61+
spec.Extensions
62+
}
63+
x.Ref = h.Refable.Ref.String()
64+
x.Extensions = internal.SanitizeExtensions(h.Extensions)
65+
x.HeaderProps = headerPropsOmitZero(h.HeaderProps)
66+
return opts.MarshalNext(enc, x)
67+
}
68+
5469
func (h *Header) UnmarshalJSON(data []byte) error {
5570
if internal.UseOptimizedJSONUnmarshalingV3 {
5671
return jsonv2.Unmarshal(data, h)
@@ -109,3 +124,19 @@ type HeaderProps struct {
109124
// Examples of the header
110125
Examples map[string]*Example `json:"examples,omitempty"`
111126
}
127+
128+
// Marshaling structure only, always edit along with corresponding
129+
// struct (or compilation will fail).
130+
type headerPropsOmitZero struct {
131+
Description string `json:"description,omitempty"`
132+
Required bool `json:"required,omitzero"`
133+
Deprecated bool `json:"deprecated,omitzero"`
134+
AllowEmptyValue bool `json:"allowEmptyValue,omitzero"`
135+
Style string `json:"style,omitempty"`
136+
Explode bool `json:"explode,omitzero"`
137+
AllowReserved bool `json:"allowReserved,omitzero"`
138+
Schema *spec.Schema `json:"schema,omitzero"`
139+
Content map[string]*MediaType `json:"content,omitempty"`
140+
Example interface{} `json:"example,omitempty"`
141+
Examples map[string]*Example `json:"examples,omitempty"`
142+
}

pkg/spec3/media_type.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type MediaType struct {
3535

3636
// MarshalJSON is a custom marshal function that knows how to encode MediaType as JSON
3737
func (m *MediaType) MarshalJSON() ([]byte, error) {
38+
if internal.UseOptimizedJSONMarshalingV3 {
39+
return internal.DeterministicMarshal(m)
40+
}
3841
b1, err := json.Marshal(m.MediaTypeProps)
3942
if err != nil {
4043
return nil, err
@@ -46,6 +49,16 @@ func (m *MediaType) MarshalJSON() ([]byte, error) {
4649
return swag.ConcatJSON(b1, b2), nil
4750
}
4851

52+
func (e *MediaType) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error {
53+
var x struct {
54+
MediaTypeProps mediaTypePropsOmitZero `json:",inline"`
55+
spec.Extensions
56+
}
57+
x.Extensions = internal.SanitizeExtensions(e.Extensions)
58+
x.MediaTypeProps = mediaTypePropsOmitZero(e.MediaTypeProps)
59+
return opts.MarshalNext(enc, x)
60+
}
61+
4962
func (m *MediaType) UnmarshalJSON(data []byte) error {
5063
if internal.UseOptimizedJSONUnmarshalingV3 {
5164
return jsonv2.Unmarshal(data, m)
@@ -84,3 +97,10 @@ type MediaTypeProps struct {
8497
// A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded
8598
Encoding map[string]*Encoding `json:"encoding,omitempty"`
8699
}
100+
101+
type mediaTypePropsOmitZero struct {
102+
Schema *spec.Schema `json:"schema,omitzero"`
103+
Example interface{} `json:"example,omitempty"`
104+
Examples map[string]*Example `json:"examples,omitempty"`
105+
Encoding map[string]*Encoding `json:"encoding,omitempty"`
106+
}

0 commit comments

Comments
 (0)