Skip to content

Commit 285135d

Browse files
authored
readOnly writeOnly validation (#599)
1 parent e56a195 commit 285135d

11 files changed

+379
-117
lines changed

openapi3/example_validation.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
package openapi3
22

3-
func validateExampleValue(input interface{}, schema *Schema) error {
4-
return schema.VisitJSON(input, MultiErrors())
3+
import "context"
4+
5+
func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
6+
opts := make([]SchemaValidationOption, 0, 2)
7+
8+
if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
9+
opts = append(opts, VisitAsRequest())
10+
} else if vo.examplesValidationAsRes {
11+
opts = append(opts, VisitAsResponse())
12+
}
13+
opts = append(opts, MultiErrors())
14+
15+
return schema.VisitJSON(input, opts...)
516
}

openapi3/example_validation_test.go

Lines changed: 121 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import (
99

1010
func TestExamplesSchemaValidation(t *testing.T) {
1111
type testCase struct {
12-
name string
13-
requestSchemaExample string
14-
responseSchemaExample string
15-
mediaTypeRequestExample string
16-
parametersExample string
17-
componentExamples string
18-
errContains string
12+
name string
13+
requestSchemaExample string
14+
responseSchemaExample string
15+
mediaTypeRequestExample string
16+
mediaTypeResponseExample string
17+
readWriteOnlyMediaTypeRequestExample string
18+
readWriteOnlyMediaTypeResponseExample string
19+
parametersExample string
20+
componentExamples string
21+
errContains string
1922
}
2023

2124
testCases := []testCase{
@@ -26,7 +29,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
2629
param1example:
2730
value: abcd
2831
`,
29-
errContains: "invalid paths: invalid path /user: invalid operation POST: param1example",
32+
errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`,
3033
},
3134
{
3235
name: "valid_parameter_examples",
@@ -41,7 +44,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
4144
parametersExample: `
4245
example: abcd
4346
`,
44-
errContains: "invalid path /user: invalid operation POST: invalid example",
47+
errContains: `invalid path /user: invalid operation POST: invalid example`,
4548
},
4649
{
4750
name: "valid_parameter_example",
@@ -64,7 +67,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
6467
email: bad
6568
password: short
6669
`,
67-
errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser",
70+
errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`,
6871
},
6972
{
7073
name: "valid_component_examples",
@@ -90,7 +93,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
9093
email: bad
9194
password: short
9295
`,
93-
errContains: "invalid path /user: invalid operation POST: invalid example",
96+
errContains: `invalid path /user: invalid operation POST: invalid example`,
9497
},
9598
{
9699
name: "valid_mediatype_examples",
@@ -109,7 +112,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
109112
110113
# missing password
111114
`,
112-
errContains: "schema \"CreateUserRequest\": invalid example",
115+
errContains: `schema "CreateUserRequest": invalid example`,
113116
},
114117
{
115118
name: "valid_schema_request_example",
@@ -127,15 +130,72 @@ func TestExamplesSchemaValidation(t *testing.T) {
127130
user_id: 1
128131
# missing access_token
129132
`,
130-
errContains: "schema \"CreateUserResponse\": invalid example",
133+
errContains: `schema "CreateUserResponse": invalid example`,
131134
},
132135
{
133136
name: "valid_schema_response_example",
134137
responseSchemaExample: `
135138
example:
136139
user_id: 1
137140
access_token: "abcd"
138-
`,
141+
`,
142+
},
143+
{
144+
name: "valid_readonly_writeonly_examples",
145+
readWriteOnlyMediaTypeRequestExample: `
146+
examples:
147+
ReadWriteOnlyRequest:
148+
$ref: '#/components/examples/ReadWriteOnlyRequestData'
149+
`,
150+
readWriteOnlyMediaTypeResponseExample: `
151+
examples:
152+
ReadWriteOnlyResponse:
153+
$ref: '#/components/examples/ReadWriteOnlyResponseData'
154+
`,
155+
componentExamples: `
156+
examples:
157+
ReadWriteOnlyRequestData:
158+
value:
159+
username: user
160+
password: password
161+
ReadWriteOnlyResponseData:
162+
value:
163+
user_id: 4321
164+
`,
165+
},
166+
{
167+
name: "invalid_readonly_request_examples",
168+
readWriteOnlyMediaTypeRequestExample: `
169+
examples:
170+
ReadWriteOnlyRequest:
171+
$ref: '#/components/examples/ReadWriteOnlyRequestData'
172+
`,
173+
componentExamples: `
174+
examples:
175+
ReadWriteOnlyRequestData:
176+
value:
177+
username: user
178+
password: password
179+
user_id: 4321
180+
`,
181+
errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`,
182+
},
183+
{
184+
name: "invalid_writeonly_response_examples",
185+
readWriteOnlyMediaTypeResponseExample: `
186+
examples:
187+
ReadWriteOnlyResponse:
188+
$ref: '#/components/examples/ReadWriteOnlyResponseData'
189+
`,
190+
componentExamples: `
191+
examples:
192+
ReadWriteOnlyResponseData:
193+
value:
194+
password: password
195+
user_id: 4321
196+
`,
197+
198+
errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`,
139199
},
140200
}
141201

@@ -198,7 +258,28 @@ paths:
198258
content:
199259
application/json:
200260
schema:
201-
$ref: "#/components/schemas/CreateUserResponse"
261+
$ref: "#/components/schemas/CreateUserResponse"`)
262+
spec.WriteString(tc.mediaTypeResponseExample)
263+
spec.WriteString(`
264+
/readWriteOnly:
265+
post:
266+
requestBody:
267+
content:
268+
application/json:
269+
schema:
270+
$ref: "#/components/schemas/ReadWriteOnlyData"
271+
required: true`)
272+
spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample)
273+
spec.WriteString(`
274+
responses:
275+
'201':
276+
description: a response
277+
content:
278+
application/json:
279+
schema:
280+
$ref: "#/components/schemas/ReadWriteOnlyData"`)
281+
spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample)
282+
spec.WriteString(`
202283
components:
203284
schemas:
204285
CreateUserRequest:`)
@@ -223,7 +304,6 @@ components:
223304
CreateUserResponse:`)
224305
spec.WriteString(tc.responseSchemaExample)
225306
spec.WriteString(`
226-
description: represents the response to a User creation
227307
required:
228308
- access_token
229309
- user_id
@@ -234,6 +314,28 @@ components:
234314
format: int64
235315
type: integer
236316
type: object
317+
ReadWriteOnlyData:
318+
required:
319+
# only required in request
320+
- username
321+
- password
322+
# only required in response
323+
- user_id
324+
properties:
325+
username:
326+
type: string
327+
default: default
328+
writeOnly: true # only sent in a request
329+
password:
330+
type: string
331+
default: default
332+
writeOnly: true # only sent in a request
333+
user_id:
334+
format: int64
335+
default: 1
336+
type: integer
337+
readOnly: true # only returned in a response
338+
type: object
237339
`)
238340
spec.WriteString(tc.componentExamples)
239341

@@ -278,7 +380,7 @@ func TestExampleObjectValidation(t *testing.T) {
278380
279381
password: validpassword
280382
`,
281-
errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive",
383+
errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`,
282384
componentExamples: `
283385
examples:
284386
BadUser:
@@ -295,7 +397,7 @@ func TestExampleObjectValidation(t *testing.T) {
295397
BadUser:
296398
description: empty user example
297399
`,
298-
errContains: "invalid components: example \"BadUser\": no value or externalValue field",
400+
errContains: `invalid components: example "BadUser": no value or externalValue field`,
299401
},
300402
{
301403
name: "value_externalValue_mutual_exclusion",
@@ -308,7 +410,7 @@ func TestExampleObjectValidation(t *testing.T) {
308410
password: validpassword
309411
externalValue: 'http://example.com/examples/example'
310412
`,
311-
errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive",
413+
errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`,
312414
},
313415
}
314416

openapi3/media_type.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
8888
return errors.New("example and examples are mutually exclusive")
8989
}
9090

91-
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
91+
if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
9292
return nil
9393
}
9494

9595
if example := mediaType.Example; example != nil {
96-
if err := validateExampleValue(example, schema.Value); err != nil {
96+
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
9797
return fmt.Errorf("invalid example: %w", err)
9898
}
9999
}
@@ -109,7 +109,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
109109
if err := v.Validate(ctx); err != nil {
110110
return fmt.Errorf("example %s: %w", k, err)
111111
}
112-
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
112+
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
113113
return fmt.Errorf("example %s: %w", k, err)
114114
}
115115
}

openapi3/parameter.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,12 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
318318
if parameter.Example != nil && parameter.Examples != nil {
319319
return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
320320
}
321-
if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
321+
322+
if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
322323
return nil
323324
}
324325
if example := parameter.Example; example != nil {
325-
if err := validateExampleValue(example, schema.Value); err != nil {
326+
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
326327
return fmt.Errorf("invalid example: %w", err)
327328
}
328329
} else if examples := parameter.Examples; examples != nil {
@@ -336,7 +337,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
336337
if err := v.Validate(ctx); err != nil {
337338
return fmt.Errorf("%s: %w", k, err)
338339
}
339-
if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
340+
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
340341
return fmt.Errorf("%s: %w", k, err)
341342
}
342343
}

openapi3/request_body.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,10 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error {
109109
if requestBody.Content == nil {
110110
return errors.New("content of the request body is required")
111111
}
112+
113+
if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
114+
vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
115+
}
116+
112117
return requestBody.Content.Validate(ctx)
113118
}

openapi3/response.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ func (response *Response) Validate(ctx context.Context) error {
115115
if response.Description == nil {
116116
return errors.New("a short description of the response is required")
117117
}
118+
if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
119+
vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
120+
}
118121

119122
if content := response.Content; content != nil {
120123
if err := content.Validate(ctx); err != nil {

openapi3/schema.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
769769
}
770770

771771
if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled {
772-
if err := validateExampleValue(x, schema); err != nil {
772+
if err := validateExampleValue(ctx, x, schema); err != nil {
773773
return fmt.Errorf("invalid example: %w", err)
774774
}
775775
}
@@ -1449,6 +1449,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
14491449
return schema.expectedType(settings, TypeObject)
14501450
}
14511451

1452+
var me MultiError
1453+
14521454
if settings.asreq || settings.asrep {
14531455
properties := make([]string, 0, len(schema.Properties))
14541456
for propName := range schema.Properties {
@@ -1457,19 +1459,28 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
14571459
sort.Strings(properties)
14581460
for _, propName := range properties {
14591461
propSchema := schema.Properties[propName]
1462+
reqRO := settings.asreq && propSchema.Value.ReadOnly
1463+
repWO := settings.asrep && propSchema.Value.WriteOnly
1464+
14601465
if value[propName] == nil {
1461-
if dlft := propSchema.Value.Default; dlft != nil {
1466+
if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO {
14621467
value[propName] = dlft
14631468
if f := settings.defaultsSet; f != nil {
14641469
settings.onceSettingDefaults.Do(f)
14651470
}
14661471
}
14671472
}
1473+
1474+
if value[propName] != nil {
1475+
if reqRO {
1476+
me = append(me, fmt.Errorf("readOnly property %q in request", propName))
1477+
} else if repWO {
1478+
me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
1479+
}
1480+
}
14681481
}
14691482
}
14701483

1471-
var me MultiError
1472-
14731484
// "properties"
14741485
properties := schema.Properties
14751486
lenValue := int64(len(value))

openapi3/validation_options.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import "context"
55
// ValidationOption allows the modification of how the OpenAPI document is validated.
66
type ValidationOption func(options *ValidationOptions)
77

8-
// ValidationOptions provide configuration for validating OpenAPI documents.
8+
// ValidationOptions provides configuration for validating OpenAPI documents.
99
type ValidationOptions struct {
10-
SchemaFormatValidationEnabled bool
11-
SchemaPatternValidationDisabled bool
12-
ExamplesValidationDisabled bool
10+
SchemaFormatValidationEnabled bool
11+
SchemaPatternValidationDisabled bool
12+
ExamplesValidationDisabled bool
13+
examplesValidationAsReq, examplesValidationAsRes bool
1314
}
1415

1516
type validationOptionsKey struct{}

0 commit comments

Comments
 (0)