Skip to content

Commit 8c2fe94

Browse files
committed
fix: optional read only and write only validations (#689)
1 parent 6e233af commit 8c2fe94

File tree

8 files changed

+310
-9
lines changed

8 files changed

+310
-9
lines changed

.github/docs/openapi3.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ type SchemaRefs []*SchemaRef
113113
type SchemaValidationOption func(*schemaValidationSettings)
114114
func DefaultsSet(f func()) SchemaValidationOption
115115
func DisablePatternValidation() SchemaValidationOption
116+
func DisableReadOnlyValidation() SchemaValidationOption
117+
func DisableWriteOnlyValidation() SchemaValidationOption
116118
func EnableFormatValidation() SchemaValidationOption
117119
func FailFast() SchemaValidationOption
118120
func MultiErrors() SchemaValidationOption

openapi3/issue689_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package openapi3_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/getkin/kin-openapi/openapi3"
9+
)
10+
11+
func TestIssue689(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := [...]struct {
15+
name string
16+
schema *openapi3.Schema
17+
value map[string]interface{}
18+
opts []openapi3.SchemaValidationOption
19+
checkErr require.ErrorAssertionFunc
20+
}{
21+
// read-only
22+
{
23+
name: "read-only property succeeds when read-only validation is disabled",
24+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
25+
"foo": {Type: "boolean", ReadOnly: true}}),
26+
value: map[string]interface{}{"foo": true},
27+
opts: []openapi3.SchemaValidationOption{
28+
openapi3.VisitAsRequest(),
29+
openapi3.DisableReadOnlyValidation()},
30+
checkErr: require.NoError,
31+
},
32+
{
33+
name: "non read-only property succeeds when read-only validation is disabled",
34+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
35+
"foo": {Type: "boolean", ReadOnly: false}}),
36+
opts: []openapi3.SchemaValidationOption{
37+
openapi3.VisitAsRequest()},
38+
value: map[string]interface{}{"foo": true},
39+
checkErr: require.NoError,
40+
},
41+
{
42+
name: "read-only property fails when read-only validation is enabled",
43+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
44+
"foo": {Type: "boolean", ReadOnly: true}}),
45+
opts: []openapi3.SchemaValidationOption{
46+
openapi3.VisitAsRequest()},
47+
value: map[string]interface{}{"foo": true},
48+
checkErr: require.Error,
49+
},
50+
{
51+
name: "non read-only property succeeds when read-only validation is enabled",
52+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
53+
"foo": {Type: "boolean", ReadOnly: false}}),
54+
opts: []openapi3.SchemaValidationOption{
55+
openapi3.VisitAsRequest()},
56+
value: map[string]interface{}{"foo": true},
57+
checkErr: require.NoError,
58+
},
59+
// write-only
60+
{
61+
name: "write-only property succeeds when write-only validation is disabled",
62+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
63+
"foo": {Type: "boolean", WriteOnly: true}}),
64+
value: map[string]interface{}{"foo": true},
65+
opts: []openapi3.SchemaValidationOption{
66+
openapi3.VisitAsResponse(),
67+
openapi3.DisableWriteOnlyValidation()},
68+
checkErr: require.NoError,
69+
},
70+
{
71+
name: "non write-only property succeeds when write-only validation is disabled",
72+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
73+
"foo": {Type: "boolean", WriteOnly: false}}),
74+
opts: []openapi3.SchemaValidationOption{
75+
openapi3.VisitAsResponse()},
76+
value: map[string]interface{}{"foo": true},
77+
checkErr: require.NoError,
78+
},
79+
{
80+
name: "write-only property fails when write-only validation is enabled",
81+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
82+
"foo": {Type: "boolean", WriteOnly: true}}),
83+
opts: []openapi3.SchemaValidationOption{
84+
openapi3.VisitAsResponse()},
85+
value: map[string]interface{}{"foo": true},
86+
checkErr: require.Error,
87+
},
88+
{
89+
name: "non write-only property succeeds when write-only validation is enabled",
90+
schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
91+
"foo": {Type: "boolean", WriteOnly: false}}),
92+
opts: []openapi3.SchemaValidationOption{
93+
openapi3.VisitAsResponse()},
94+
value: map[string]interface{}{"foo": true},
95+
checkErr: require.NoError,
96+
},
97+
}
98+
99+
for _, test := range tests {
100+
test := test
101+
t.Run(test.name, func(t *testing.T) {
102+
t.Parallel()
103+
err := test.schema.VisitJSON(test.value, test.opts...)
104+
test.checkErr(t, err)
105+
})
106+
}
107+
}

openapi3/schema.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,8 +1787,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
17871787
sort.Strings(properties)
17881788
for _, propName := range properties {
17891789
propSchema := schema.Properties[propName]
1790-
reqRO := settings.asreq && propSchema.Value.ReadOnly
1791-
repWO := settings.asrep && propSchema.Value.WriteOnly
1790+
reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled
1791+
repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled
17921792

17931793
if value[propName] == nil {
17941794
if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO {

openapi3/schema_validation_settings.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import (
88
type SchemaValidationOption func(*schemaValidationSettings)
99

1010
type schemaValidationSettings struct {
11-
failfast bool
12-
multiError bool
13-
asreq, asrep bool // exclusive (XOR) fields
14-
formatValidationEnabled bool
15-
patternValidationDisabled bool
11+
failfast bool
12+
multiError bool
13+
asreq, asrep bool // exclusive (XOR) fields
14+
formatValidationEnabled bool
15+
patternValidationDisabled bool
16+
readOnlyValidationDisabled bool
17+
writeOnlyValidationDisabled bool
1618

1719
onceSettingDefaults sync.Once
1820
defaultsSet func()
@@ -47,6 +49,16 @@ func DisablePatternValidation() SchemaValidationOption {
4749
return func(s *schemaValidationSettings) { s.patternValidationDisabled = true }
4850
}
4951

52+
// DisableReadOnlyValidation setting makes Validate not return an error when validating properties marked as read-only
53+
func DisableReadOnlyValidation() SchemaValidationOption {
54+
return func(s *schemaValidationSettings) { s.readOnlyValidationDisabled = true }
55+
}
56+
57+
// DisableWriteOnlyValidation setting makes Validate not return an error when validating properties marked as write-only
58+
func DisableWriteOnlyValidation() SchemaValidationOption {
59+
return func(s *schemaValidationSettings) { s.writeOnlyValidationDisabled = true }
60+
}
61+
5062
// DefaultsSet executes the given callback (once) IFF schema validation set default values.
5163
func DefaultsSet(f func()) SchemaValidationOption {
5264
return func(s *schemaValidationSettings) { s.defaultsSet = f }

openapi3filter/issue689_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package openapi3filter
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/getkin/kin-openapi/openapi3"
12+
"github.com/getkin/kin-openapi/routers/gorillamux"
13+
)
14+
15+
func TestIssue689(t *testing.T) {
16+
loader := openapi3.NewLoader()
17+
ctx := loader.Context
18+
spec := `
19+
openapi: 3.0.0
20+
info:
21+
version: 1.0.0
22+
title: Sample API
23+
paths:
24+
/items:
25+
put:
26+
requestBody:
27+
content:
28+
application/json:
29+
schema:
30+
properties:
31+
testWithReadOnly:
32+
readOnly: true
33+
type: boolean
34+
testNoReadOnly:
35+
type: boolean
36+
type: object
37+
responses:
38+
'200':
39+
description: OK
40+
get:
41+
responses:
42+
'200':
43+
description: OK
44+
content:
45+
application/json:
46+
schema:
47+
properties:
48+
testWithWriteOnly:
49+
writeOnly: true
50+
type: boolean
51+
testNoWriteOnly:
52+
type: boolean
53+
`[1:]
54+
55+
doc, err := loader.LoadFromData([]byte(spec))
56+
require.NoError(t, err)
57+
58+
err = doc.Validate(ctx)
59+
require.NoError(t, err)
60+
61+
router, err := gorillamux.NewRouter(doc)
62+
require.NoError(t, err)
63+
64+
tests := []struct {
65+
name string
66+
options *Options
67+
body string
68+
method string
69+
checkErr require.ErrorAssertionFunc
70+
}{
71+
// read-only
72+
{
73+
name: "non read-only property is added to request when validation enabled",
74+
body: `{"testNoReadOnly": true}`,
75+
method: http.MethodPut,
76+
checkErr: require.NoError,
77+
},
78+
{
79+
name: "non read-only property is added to request when validation disabled",
80+
body: `{"testNoReadOnly": true}`,
81+
method: http.MethodPut,
82+
options: &Options{
83+
ExcludeReadOnlyValidations: true,
84+
},
85+
checkErr: require.NoError,
86+
},
87+
{
88+
name: "read-only property is added to requests when validation enabled",
89+
body: `{"testWithReadOnly": true}`,
90+
method: http.MethodPut,
91+
checkErr: require.Error,
92+
},
93+
{
94+
name: "read-only property is added to requests when validation disabled",
95+
body: `{"testWithReadOnly": true}`,
96+
method: http.MethodPut,
97+
options: &Options{
98+
ExcludeReadOnlyValidations: true,
99+
},
100+
checkErr: require.NoError,
101+
},
102+
// write-only
103+
{
104+
name: "non write-only property is added to request when validation enabled",
105+
body: `{"testNoWriteOnly": true}`,
106+
method: http.MethodGet,
107+
checkErr: require.NoError,
108+
},
109+
{
110+
name: "non write-only property is added to request when validation disabled",
111+
body: `{"testNoWriteOnly": true}`,
112+
method: http.MethodGet,
113+
options: &Options{
114+
ExcludeWriteOnlyValidations: true,
115+
},
116+
checkErr: require.NoError,
117+
},
118+
{
119+
name: "write-only property is added to requests when validation enabled",
120+
body: `{"testWithWriteOnly": true}`,
121+
method: http.MethodGet,
122+
checkErr: require.Error,
123+
},
124+
{
125+
name: "write-only property is added to requests when validation disabled",
126+
body: `{"testWithWriteOnly": true}`,
127+
method: http.MethodGet,
128+
options: &Options{
129+
ExcludeWriteOnlyValidations: true,
130+
},
131+
checkErr: require.NoError,
132+
},
133+
}
134+
135+
for _, test := range tests {
136+
t.Run(test.name, func(t *testing.T) {
137+
httpReq, err := http.NewRequest(test.method, "/items", strings.NewReader(test.body))
138+
require.NoError(t, err)
139+
httpReq.Header.Set("Content-Type", "application/json")
140+
require.NoError(t, err)
141+
142+
route, pathParams, err := router.FindRoute(httpReq)
143+
require.NoError(t, err)
144+
145+
requestValidationInput := &RequestValidationInput{
146+
Request: httpReq,
147+
PathParams: pathParams,
148+
Route: route,
149+
Options: test.options,
150+
}
151+
152+
if test.method == http.MethodGet {
153+
responseValidationInput := &ResponseValidationInput{
154+
RequestValidationInput: requestValidationInput,
155+
Status: 200,
156+
Header: httpReq.Header,
157+
Body: io.NopCloser(strings.NewReader(test.body)),
158+
Options: test.options,
159+
}
160+
err = ValidateResponse(ctx, responseValidationInput)
161+
162+
} else {
163+
err = ValidateRequest(ctx, requestValidationInput)
164+
}
165+
test.checkErr(t, err)
166+
})
167+
}
168+
}

openapi3filter/options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ type Options struct {
1515
// Set ExcludeResponseBody so ValidateResponse skips response body validation
1616
ExcludeResponseBody bool
1717

18+
// Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations
19+
ExcludeReadOnlyValidations bool
20+
21+
// Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations
22+
ExcludeWriteOnlyValidations bool
23+
1824
// Set IncludeResponseStatus so ValidateResponse fails on response
1925
// status not defined in OpenAPI spec
2026
IncludeResponseStatus bool

openapi3filter/validate_request.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
272272
}
273273

274274
defaultsSet := false
275-
opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here
275+
opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here
276276
opts = append(opts, openapi3.VisitAsRequest())
277277
if !options.SkipSettingDefaults {
278278
opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
@@ -283,6 +283,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
283283
if options.customSchemaErrorFunc != nil {
284284
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
285285
}
286+
if options.ExcludeReadOnlyValidations {
287+
opts = append(opts, openapi3.DisableReadOnlyValidation())
288+
}
286289

287290
// Validate JSON with the schema
288291
if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil {

openapi3filter/validate_response.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
6363
return &ResponseError{Input: input, Reason: "response has not been resolved"}
6464
}
6565

66-
opts := make([]openapi3.SchemaValidationOption, 0, 2)
66+
opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here
6767
if options.MultiError {
6868
opts = append(opts, openapi3.MultiErrors())
6969
}
7070
if options.customSchemaErrorFunc != nil {
7171
opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
7272
}
73+
if options.ExcludeWriteOnlyValidations {
74+
opts = append(opts, openapi3.DisableWriteOnlyValidation())
75+
}
7376

7477
headers := make([]string, 0, len(response.Headers))
7578
for k := range response.Headers {

0 commit comments

Comments
 (0)