From 0e202c6e9214fc702a4927616dc445ea939b8dc8 Mon Sep 17 00:00:00 2001 From: Christian Boitel Date: Mon, 31 Jul 2023 13:37:42 +0200 Subject: [PATCH] feat getkin#582: use OpenAPI minor version during validation when available --- .github/docs/openapi3.txt | 5 + openapi3/issue735_test.go | 2 + openapi3/schema.go | 30 +- openapi3/schema_issue492_test.go | 2 +- openapi3/schema_test.go | 1085 +++++++++++++++--------- openapi3/schema_validation_settings.go | 7 + openapi3filter/middleware_test.go | 250 +++++- openapi3filter/validate_request.go | 6 + openapi3filter/validate_response.go | 5 +- 9 files changed, 953 insertions(+), 439 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 094d48af4..637972e54 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -100,8 +100,12 @@ type Schema struct{ ... } func NewArraySchema() *Schema func NewBoolSchema() *Schema func NewBytesSchema() *Schema + func NewDateSchema() *Schema func NewDateTimeSchema() *Schema func NewFloat64Schema() *Schema + func NewHostnameSchema() *Schema + func NewIPv4Schema() *Schema + func NewIPv6Schema() *Schema func NewInt32Schema() *Schema func NewInt64Schema() *Schema func NewIntegerSchema() *Schema @@ -125,6 +129,7 @@ type SchemaValidationOption func(*schemaValidationSettings) func EnableFormatValidation() SchemaValidationOption func FailFast() SchemaValidationOption func MultiErrors() SchemaValidationOption + func SetOpenAPIMinorVersion(minorVersion uint64) SchemaValidationOption func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption func VisitAsRequest() SchemaValidationOption func VisitAsResponse() SchemaValidationOption diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go index f7e420c5d..f3da3a797 100644 --- a/openapi3/issue735_test.go +++ b/openapi3/issue735_test.go @@ -20,6 +20,8 @@ func TestIssue735(t *testing.T) { DefineStringFormat("email", FormatOfStringForEmail) DefineIPv4Format() DefineIPv6Format() + // restore modified string formats used during this tests + defer RestoreDefaultStringFormats() testCases := []testCase{ { diff --git a/openapi3/schema.go b/openapi3/schema.go index 0dde6073d..c5292917b 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -579,6 +579,13 @@ func NewStringSchema() *Schema { } } +func NewDateSchema() *Schema { + return &Schema{ + Type: TypeString, + Format: "date", + } +} + func NewDateTimeSchema() *Schema { return &Schema{ Type: TypeString, @@ -586,6 +593,13 @@ func NewDateTimeSchema() *Schema { } } +func NewHostnameSchema() *Schema { + return &Schema{ + Type: TypeString, + Format: "hostname", + } +} + func NewUUIDSchema() *Schema { return &Schema{ Type: TypeString, @@ -593,6 +607,20 @@ func NewUUIDSchema() *Schema { } } +func NewIPv4Schema() *Schema { + return &Schema{ + Type: TypeString, + Format: "ipv4", + } +} + +func NewIPv6Schema() *Schema { + return &Schema{ + Type: TypeString, + Format: "ipv6", + } +} + func NewBytesSchema() *Schema { return &Schema{ Type: TypeString, @@ -1624,7 +1652,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value var formatStrErr string var formatErr error if format := schema.Format; format != "" { - if f, ok := SchemaStringFormats[format]; ok { + if f := getSchemaStringFormats(format, settings.openapiMinorVersion); f != nil { switch { case f.regexp != nil && f.callback == nil: if cp := f.regexp; !cp.MatchString(value) { diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go index 4ad72abc9..c57db5bb3 100644 --- a/openapi3/schema_issue492_test.go +++ b/openapi3/schema_issue492_test.go @@ -46,5 +46,5 @@ info: "name": "kin-openapi", "time": "2001-02-03T04:05:06:789Z", }) - require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time" (regular expression "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$")`) + require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time" (regular expression `) } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 1c2ed355f..f32d9fe3c 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -17,12 +18,11 @@ type schemaExample struct { Title string Schema *Schema Serialization interface{} - AllValid []interface{} - AllInvalid []interface{} + AllValid [][]interface{} // indexed by minor OpenAPI version supported + AllInvalid [][]interface{} // also indexed by minor OpenAPI version supported } func TestSchemas(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) for _, example := range schemaExamples { t.Run(example.Title, testSchema(t, example)) } @@ -46,19 +46,23 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { require.Equal(t, dataUnserialized, dataSchema) } for validateFuncIndex, validateFunc := range validateSchemaFuncs { - for index, value := range example.AllValid { - err := validateFunc(t, schema, value) - require.NoErrorf(t, err, "ValidateFunc #%d, AllValid #%d: %#v", validateFuncIndex, index, value) + for minorVersion, examples := range example.AllValid { + for index, value := range examples { + err := validateFunc(t, schema, value, SetOpenAPIMinorVersion(uint64(minorVersion))) + assert.NoErrorf(t, err, "ValidateFunc #%d, AllValid #%d: %#v", validateFuncIndex, index, value) + } } - for index, value := range example.AllInvalid { - err := validateFunc(t, schema, value) - require.Errorf(t, err, "ValidateFunc #%d, AllInvalid #%d: %#v", validateFuncIndex, index, value) + for minorVersion, examples := range example.AllInvalid { + for index, value := range examples { + err := validateFunc(t, schema, value, SetOpenAPIMinorVersion(uint64(minorVersion))) + assert.Errorf(t, err, "ValidateFunc #%d, AllInvalid #%d: %#v", validateFuncIndex, index, value) + } } } // NaN and Inf aren't valid JSON but are handled for index, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { err := schema.VisitJSON(value) - require.Errorf(t, err, "NaNAndInf #%d: %#v", index, value) + assert.Errorf(t, err, "NaNAndInf #%d: %#v", index, value) } } } @@ -96,16 +100,20 @@ var schemaExamples = []schemaExample{ // This OA3 schema is exactly this draft-04 schema: // {"not": {"type": "null"}} }, - AllValid: []interface{}{ - false, - true, - 3.14, - "", - []interface{}{}, - map[string]interface{}{}, + AllValid: [][]interface{}{ + { // OpenAPI 3.0 + false, + true, + 3.14, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, - AllInvalid: []interface{}{ - nil, + AllInvalid: [][]interface{}{ + { // OpenAPI 3.0 + nil, + }, }, }, @@ -118,16 +126,18 @@ var schemaExamples = []schemaExample{ // ,{type:array, items:{}}, type:object]} "nullable": true, }, - AllValid: []interface{}{ - nil, - false, - true, - 0, - 0.0, - 3.14, - "", - []interface{}{}, - map[string]interface{}{}, + AllValid: [][]interface{}{ + { + nil, + false, + true, + 0, + 0.0, + 3.14, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, @@ -138,18 +148,22 @@ var schemaExamples = []schemaExample{ "nullable": true, "type": "boolean", }, - AllValid: []interface{}{ - nil, - false, - true, + AllValid: [][]interface{}{ + { + nil, + false, + true, + }, }, - AllInvalid: []interface{}{ - 0, - 0.0, - 3.14, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + 0, + 0.0, + 3.14, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, @@ -166,16 +180,20 @@ var schemaExamples = []schemaExample{ map[string]interface{}{"type": "number"}, }, }, - AllValid: []interface{}{ - nil, - 42, - 4.2, + AllValid: [][]interface{}{ + { + nil, + 42, + 4.2, + }, }, - AllInvalid: []interface{}{ - true, - []interface{}{42}, - "bla", - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + true, + []interface{}{42}, + "bla", + map[string]interface{}{}, + }, }, }, @@ -185,16 +203,20 @@ var schemaExamples = []schemaExample{ Serialization: map[string]interface{}{ "type": "boolean", }, - AllValid: []interface{}{ - false, - true, + AllValid: [][]interface{}{ + { + false, + true, + }, }, - AllInvalid: []interface{}{ - nil, - 3.14, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, @@ -208,20 +230,24 @@ var schemaExamples = []schemaExample{ "minimum": 2.5, "maximum": 3.5, }, - AllValid: []interface{}{ - 2.5, - 3.14, - 3.5, + AllValid: [][]interface{}{ + { + 2.5, + 3.14, + 3.5, + }, }, - AllInvalid: []interface{}{ - nil, - false, - true, - 2.4, - 3.6, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + false, + true, + 2.4, + 3.6, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, @@ -236,20 +262,24 @@ var schemaExamples = []schemaExample{ "minimum": 2, "maximum": 5, }, - AllValid: []interface{}{ - 2, - 5, + AllValid: [][]interface{}{ + { + 2, + 5, + }, }, - AllInvalid: []interface{}{ - nil, - false, - true, - 1, - 6, - 3.5, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + false, + true, + 1, + 6, + 3.5, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, { @@ -259,21 +289,25 @@ var schemaExamples = []schemaExample{ "type": "integer", "format": "int64", }, - AllValid: []interface{}{ - 1, - 256, - 65536, - int64(math.MaxInt32) + 10, - int64(math.MinInt32) - 10, + AllValid: [][]interface{}{ + { + 1, + 256, + 65536, + int64(math.MaxInt32) + 10, + int64(math.MinInt32) - 10, + }, }, - AllInvalid: []interface{}{ - nil, - false, - 3.5, - true, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + false, + 3.5, + true, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, { @@ -283,23 +317,27 @@ var schemaExamples = []schemaExample{ "type": "integer", "format": "int32", }, - AllValid: []interface{}{ - 1, - 256, - 65536, - int64(math.MaxInt32), - int64(math.MaxInt32), + AllValid: [][]interface{}{ + { + 1, + 256, + 65536, + int64(math.MaxInt32), + int64(math.MaxInt32), + }, }, - AllInvalid: []interface{}{ - nil, - false, - 3.5, - int64(math.MaxInt32) + 10, - int64(math.MinInt32) - 10, - true, - "", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + false, + 3.5, + int64(math.MaxInt32) + 10, + int64(math.MinInt32) - 10, + true, + "", + []interface{}{}, + map[string]interface{}{}, + }, }, }, { @@ -314,23 +352,26 @@ var schemaExamples = []schemaExample{ "maxLength": 3, "pattern": "^[abc]+$", }, - AllValid: []interface{}{ - "ab", - "abc", + AllValid: [][]interface{}{ + { + "ab", + "abc", + }, }, - AllInvalid: []interface{}{ - nil, - false, - true, - 3.14, - "a", - "xy", - "aaaa", - []interface{}{}, - map[string]interface{}{}, + AllInvalid: [][]interface{}{ + { + nil, + false, + true, + 3.14, + "a", + "xy", + "aaaa", + []interface{}{}, + map[string]interface{}{}, + }, }, }, - { Title: "STRING: optional format 'uuid'", Schema: NewUUIDSchema(), @@ -338,30 +379,67 @@ var schemaExamples = []schemaExample{ "type": "string", "format": "uuid", }, - AllValid: []interface{}{ - "dd7d8481-81a3-407f-95f0-a2f1cb382a4b", - "dcba3901-2fba-48c1-9db2-00422055804e", - "ace8e3be-c254-4c10-8859-1401d9a9d52a", - "DD7D8481-81A3-407F-95F0-A2F1CB382A4B", - "DCBA3901-2FBA-48C1-9DB2-00422055804E", - "ACE8E3BE-C254-4C10-8859-1401D9A9D52A", - "dd7D8481-81A3-407f-95F0-A2F1CB382A4B", - "DCBA3901-2FBA-48C1-9db2-00422055804e", - "ACE8E3BE-c254-4C10-8859-1401D9A9D52A", - }, - AllInvalid: []interface{}{ + AllValid: [][]interface{}{ + { + "anything-not-definedin3.0", + }, + { + "dd7d8481-81a3-407f-95f0-a2f1cb382a4b", + "dcba3901-2fba-48c1-9db2-00422055804e", + "ace8e3be-c254-4c10-8859-1401d9a9d52a", + "DD7D8481-81A3-407F-95F0-A2F1CB382A4B", + "DCBA3901-2FBA-48C1-9DB2-00422055804E", + "ACE8E3BE-C254-4C10-8859-1401D9A9D52A", + "dd7D8481-81A3-407f-95F0-A2F1CB382A4B", + "DCBA3901-2FBA-48C1-9db2-00422055804e", + "ACE8E3BE-c254-4C10-8859-1401D9A9D52A", + }, + }, + AllInvalid: [][]interface{}{ nil, - "g39840b1-d0ef-446d-e555-48fcca50a90a", - "4cf3i040-ea14-4daa-b0b5-ea9329473519", - "aaf85740-7e27-4b4f-b4554-a03a43b1f5e3", - "56f5bff4-z4b6-48e6-a10d-b6cf66a83b04", - "G39840B1-D0EF-446D-E555-48FCCA50A90A", - "4CF3I040-EA14-4DAA-B0B5-EA9329473519", - "AAF85740-7E27-4B4F-B4554-A03A43B1F5E3", - "56F5BFF4-Z4B6-48E6-A10D-B6CF66A83B04", - "4CF3I040-EA14-4Daa-B0B5-EA9329473519", - "AAf85740-7E27-4B4F-B4554-A03A43b1F5E3", - "56F5Bff4-Z4B6-48E6-a10D-B6CF66A83B04", + { + nil, + "g39840b1-d0ef-446d-e555-48fcca50a90a", + "4cf3i040-ea14-4daa-b0b5-ea9329473519", + "aaf85740-7e27-4b4f-b4554-a03a43b1f5e3", + "56f5bff4-z4b6-48e6-a10d-b6cf66a83b04", + "G39840B1-D0EF-446D-E555-48FCCA50A90A", + "4CF3I040-EA14-4DAA-B0B5-EA9329473519", + "AAF85740-7E27-4B4F-B4554-A03A43B1F5E3", + "56F5BFF4-Z4B6-48E6-A10D-B6CF66A83B04", + "4CF3I040-EA14-4Daa-B0B5-EA9329473519", + "AAf85740-7E27-4B4F-B4554-A03A43b1F5E3", + "56F5Bff4-Z4B6-48E6-a10D-B6CF66A83B04", + }, + }, + }, + + { + Title: "STRING: format 'date'", + Schema: NewDateSchema(), + Serialization: map[string]interface{}{ + "type": "string", + "format": "date", + }, + AllValid: [][]interface{}{ + { + "2017-12-31", + "2017-01-01", + }, + }, + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + "2017-12-00", + "2017-12-32", + "2017-13-01", + "2017-00-31", + "99-09-09", + "2017-01-00", + "2017-01-32", + "2017-01-40", + }, }, }, @@ -372,53 +450,68 @@ var schemaExamples = []schemaExample{ "type": "string", "format": "date-time", }, - AllValid: []interface{}{ - "2017-12-31T11:59:59", - "2017-12-31T11:59:59Z", - "2017-12-31T11:59:59-11:30", - "2017-12-31T11:59:59+11:30", - "2017-12-31T11:59:59.999+11:30", - "2017-12-31T11:59:59.999Z", + AllValid: [][]interface{}{ + { + "2017-12-31T11:59:59", + "2017-12-31T11:59:59Z", + "2017-12-31T11:59:59-11:30", + "2017-12-31T11:59:59+11:30", + "2017-12-31T11:59:59.999+11:30", + "2017-12-31T11:59:59.999Z", + "2017-12-31T23:59:60", // leap second + }, }, - AllInvalid: []interface{}{ - nil, - 3.14, - "2017-12-31", - "2017-12-31T11:59:59\n", - "2017-12-31T11:59:59.+11:30", - "2017-12-31T11:59:59.Z", + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + "2017-12-31", + "2017-12-31T11:59:59\n", + "2017-12-31T11:59:59.+11:30", + "2017-12-31T11:59:59.Z", + "2017-12-00T11:59:59.Z", + "2017-12-32T11:59:59.Z", + "2017-12-40T11:59:59.Z", + "2017-12-00T11:59:59", + "2017-12-31T11:59:60", + "99-09-09T11:59:59", + }, }, }, { - Title: "STRING: format 'date-time'", + Title: "STRING: format 'byte'", Schema: NewBytesSchema(), Serialization: map[string]interface{}{ "type": "string", "format": "byte", }, - AllValid: []interface{}{ - "", - base64.StdEncoding.EncodeToString(func() []byte { - data := make([]byte, 0, 1024) - for i := 0; i < cap(data); i++ { - data = append(data, byte(i)) - } - return data - }()), - base64.URLEncoding.EncodeToString(func() []byte { - data := make([]byte, 0, 1024) - for i := 0; i < cap(data); i++ { - data = append(data, byte(i)) - } - return data - }()), + AllValid: [][]interface{}{ + { + "", + base64.StdEncoding.EncodeToString(func() []byte { + data := make([]byte, 0, 1024) + for i := 0; i < cap(data); i++ { + data = append(data, byte(i)) + } + return data + }()), + base64.URLEncoding.EncodeToString(func() []byte { + data := make([]byte, 0, 1024) + for i := 0; i < cap(data); i++ { + data = append(data, byte(i)) + } + return data + }()), + }, }, - AllInvalid: []interface{}{ - nil, - " ", - "\n\n", // a \n is ok for JSON but not for YAML decoder/encoder - "%", + AllInvalid: [][]interface{}{ + { + nil, + " ", + "\n\n", // a \n is ok for JSON but not for YAML decoder/encoder + "%", + }, }, }, @@ -440,25 +533,29 @@ var schemaExamples = []schemaExample{ "type": "number", }, }, - AllValid: []interface{}{ - []interface{}{ - 1, 2, - }, - []interface{}{ - 1, 2, 3, + AllValid: [][]interface{}{ + { + []interface{}{ + 1, 2, + }, + []interface{}{ + 1, 2, 3, + }, }, }, - AllInvalid: []interface{}{ - nil, - 3.14, - []interface{}{ - 1, - }, - []interface{}{ - 42, 42, - }, - []interface{}{ - 1, 2, 3, 4, + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + []interface{}{ + 1, + }, + []interface{}{ + 42, 42, + }, + []interface{}{ + 1, 2, 3, 4, + }, }, }, }, @@ -486,34 +583,38 @@ var schemaExamples = []schemaExample{ "type": "object", }, }, - AllValid: []interface{}{ - []interface{}{ - map[string]interface{}{ - "key1": 1, - "key2": 1, - // Additional properties will make object different - // By default additionalProperties is true - }, - map[string]interface{}{ - "key1": 1, - }, - }, - []interface{}{ - map[string]interface{}{ - "key1": 1, + AllValid: [][]interface{}{ + { + []interface{}{ + map[string]interface{}{ + "key1": 1, + "key2": 1, + // Additioanl properties will make object different + // By default additionalProperties is true + }, + map[string]interface{}{ + "key1": 1, + }, }, - map[string]interface{}{ - "key1": 2, + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 2, + }, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ - map[string]interface{}{ - "key1": 1, - }, - map[string]interface{}{ - "key1": 1, + AllInvalid: [][]interface{}{ + { + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 1, + }, }, }, }, @@ -551,54 +652,58 @@ var schemaExamples = []schemaExample{ "type": "object", }, }, - AllValid: []interface{}{ - []interface{}{ - map[string]interface{}{ - "key1": []interface{}{ - 1, 2, + AllValid: [][]interface{}{ + { + []interface{}{ + map[string]interface{}{ + "key1": []interface{}{ + 1, 2, + }, }, - }, - map[string]interface{}{ - "key1": []interface{}{ - 3, 4, + map[string]interface{}{ + "key1": []interface{}{ + 3, 4, + }, }, }, - }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices - map[string]interface{}{ - "key1": []interface{}{ - 10, 9, + []interface{}{ // Slice have items with the same value but with different index will treated as different slices + map[string]interface{}{ + "key1": []interface{}{ + 10, 9, + }, }, - }, - map[string]interface{}{ - "key1": []interface{}{ - 9, 10, + map[string]interface{}{ + "key1": []interface{}{ + 9, 10, + }, }, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - map[string]interface{}{ - "key1": []interface{}{ - 9, 9, + AllInvalid: [][]interface{}{ + { + []interface{}{ // Violate outer array uniqueItems: true + map[string]interface{}{ + "key1": []interface{}{ + 9, 9, + }, }, - }, - map[string]interface{}{ - "key1": []interface{}{ - 9, 9, + map[string]interface{}{ + "key1": []interface{}{ + 9, 9, + }, }, }, - }, - []interface{}{ // Violate inner(array in object) array uniqueItems: true - map[string]interface{}{ - "key1": []interface{}{ - 9, 9, + []interface{}{ // Violate inner(array in object) array uniqueItems: true + map[string]interface{}{ + "key1": []interface{}{ + 9, 9, + }, }, - }, - map[string]interface{}{ - "key1": []interface{}{ - 8, 8, + map[string]interface{}{ + "key1": []interface{}{ + 8, 8, + }, }, }, }, @@ -627,24 +732,28 @@ var schemaExamples = []schemaExample{ "type": "array", }, }, - AllValid: []interface{}{ - []interface{}{ - []interface{}{1, 2}, - []interface{}{3, 4}, - }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices - []interface{}{1, 2}, - []interface{}{2, 1}, + AllValid: [][]interface{}{ + { + []interface{}{ + []interface{}{1, 2}, + []interface{}{3, 4}, + }, + []interface{}{ // Slice have items with the same value but with different index will treated as different slices + []interface{}{1, 2}, + []interface{}{2, 1}, + }, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - []interface{}{8, 9}, - []interface{}{8, 9}, - }, - []interface{}{ // Violate inner array uniqueItems: true - []interface{}{9, 9}, - []interface{}{8, 8}, + AllInvalid: [][]interface{}{ + { + []interface{}{ // Violate outer array uniqueItems: true + []interface{}{8, 9}, + []interface{}{8, 9}, + }, + []interface{}{ // Violate inner array uniqueItems: true + []interface{}{9, 9}, + []interface{}{8, 8}, + }, }, }, }, @@ -681,72 +790,76 @@ var schemaExamples = []schemaExample{ "type": "array", }, }, - AllValid: []interface{}{ - []interface{}{ - []interface{}{ - map[string]interface{}{ - "key1": 1, - }, - }, - []interface{}{ - map[string]interface{}{ - "key1": 2, - }, - }, - }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices + AllValid: [][]interface{}{ + { []interface{}{ - map[string]interface{}{ - "key1": 1, + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, }, - map[string]interface{}{ - "key1": 2, + []interface{}{ + map[string]interface{}{ + "key1": 2, + }, }, }, - []interface{}{ - map[string]interface{}{ - "key1": 2, + []interface{}{ // Slice have items with the same value but with different index will treated as different slices + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 2, + }, }, - map[string]interface{}{ - "key1": 1, + []interface{}{ + map[string]interface{}{ + "key1": 2, + }, + map[string]interface{}{ + "key1": 1, + }, }, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - []interface{}{ - map[string]interface{}{ - "key1": 1, - }, - map[string]interface{}{ - "key1": 2, - }, - }, - []interface{}{ - map[string]interface{}{ - "key1": 1, - }, - map[string]interface{}{ - "key1": 2, - }, - }, - }, - []interface{}{ // Violate inner array uniqueItems: true - []interface{}{ - map[string]interface{}{ - "key1": 1, + AllInvalid: [][]interface{}{ + { + []interface{}{ // Violate outer array uniqueItems: true + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 2, + }, }, - map[string]interface{}{ - "key1": 1, + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 2, + }, }, }, - []interface{}{ - map[string]interface{}{ - "key1": 2, + []interface{}{ // Violate inner array uniqueItems: true + []interface{}{ + map[string]interface{}{ + "key1": 1, + }, + map[string]interface{}{ + "key1": 1, + }, }, - map[string]interface{}{ - "key1": 2, + []interface{}{ + map[string]interface{}{ + "key1": 2, + }, + map[string]interface{}{ + "key1": 2, + }, }, }, }, @@ -771,30 +884,34 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ - "numberProperty": 3.14, - }, - map[string]interface{}{ - "numberProperty": 3.14, - "some prop": nil, + AllValid: [][]interface{}{ + { + map[string]interface{}{}, + map[string]interface{}{ + "numberProperty": 3.14, + }, + map[string]interface{}{ + "numberProperty": 3.14, + "some prop": nil, + }, }, }, - AllInvalid: []interface{}{ - nil, - false, - true, - 3.14, - "", - []interface{}{}, - map[string]interface{}{ - "numberProperty": "abc", - }, - map[string]interface{}{ - "numberProperty": 3.14, - "some prop": 42, - "third": "prop", + AllInvalid: [][]interface{}{ + { + nil, + false, + true, + 3.14, + "", + []interface{}{}, + map[string]interface{}{ + "numberProperty": "abc", + }, + map[string]interface{}{ + "numberProperty": 3.14, + "some prop": 42, + "third": "prop", + }, }, }, }, @@ -813,16 +930,20 @@ var schemaExamples = []schemaExample{ "type": "number", }, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ - "x": 3.14, - "y": 3.14, + AllValid: [][]interface{}{ + { + map[string]interface{}{}, + map[string]interface{}{ + "x": 3.14, + "y": 3.14, + }, }, }, - AllInvalid: []interface{}{ - map[string]interface{}{ - "x": "abc", + AllInvalid: [][]interface{}{ + { + map[string]interface{}{ + "x": "abc", + }, }, }, }, @@ -835,11 +956,13 @@ var schemaExamples = []schemaExample{ "type": "object", "additionalProperties": true, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ - "x": false, - "y": 3.14, + AllValid: [][]interface{}{ + { + map[string]interface{}{}, + map[string]interface{}{ + "x": false, + "y": 3.14, + }, }, }, }, @@ -868,16 +991,20 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ - false, - 2, - "abc", + AllValid: [][]interface{}{ + { + false, + 2, + "abc", + }, }, - AllInvalid: []interface{}{ - nil, - true, - 3.14, - "not this", + AllInvalid: [][]interface{}{ + { + nil, + true, + 3.14, + "not this", + }, }, }, @@ -911,14 +1038,18 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ - 1, - 2, - 3, + AllValid: [][]interface{}{ + { + 1, + 2, + 3, + }, }, - AllInvalid: []interface{}{ - 0, - 4, + AllInvalid: [][]interface{}{ + { + 0, + 4, + }, }, }, @@ -952,14 +1083,18 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ - 2, + AllValid: [][]interface{}{ + { + 2, + }, }, - AllInvalid: []interface{}{ - 0, - 1, - 3, - 4, + AllInvalid: [][]interface{}{ + { + 0, + 1, + 3, + 4, + }, }, }, @@ -993,14 +1128,168 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ - 1, - 3, + AllValid: [][]interface{}{ + { + 1, + 3, + }, + }, + AllInvalid: [][]interface{}{ + { + 0, + 2, + 4, + }, + }, + }, + { + Title: "STRING: format 'hostname'", + Schema: NewHostnameSchema(), + Serialization: map[string]interface{}{ + "type": "string", + "format": "hostname", + }, + AllValid: [][]interface{}{ + { // OpenAPI 3.0: hostname format not define so anything is fine + "a", + "ab", + "a_b", + }, + { + "abc", + "a-b", + "a0b", + "ab9", + "0ab9", + "a-b.domain", + "a-b.sub-domain.domain", + "0.1", + }, + }, + AllInvalid: [][]interface{}{ + nil, + { + nil, + 3.14, + "a", + "ab", + "a_b", + "~test", + }, + }, + }, + + { + Title: "STRING: format 'ipv4'", + Schema: NewIPv4Schema(), + Serialization: map[string]interface{}{ + "type": "string", + "format": "ipv4", + }, + AllValid: [][]interface{}{ + { // OpenAPI 3.0: hostname format not define so anything is fine + "notchecked", + "pi", + }, + { + "127.0.0.1", + "192.168.1.2", + "192.168.1.0", + "10.1.2.3", + }, + }, + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + false, + 1, + }, + { + nil, + 3.14, + true, + 2, + "a.b.c.d", + "192.168.1.y", + // "192.168.1.02", // fixed in go 1.17 + // "192.168.01.2", // fixed in go 1.17 + // "10.01.2.3", // fixed in go 1.17 + // "010.1.2.3", // fixed in go 1.17 + "256.168.1.2", + "192.256.1.2", + "192.168.256.2", + "192.168.1.256", + "255", + "1.2", + "1.1.1.1.", + "-1.2.3.4", + "1...4", + "1.2..4", + "1..3.4", + "1.2.3.4.5", + ".2.3.4", + }, + }, + }, + + { + Title: "STRING: format 'ipv6'", + Schema: NewIPv6Schema(), + Serialization: map[string]interface{}{ + "type": "string", + "format": "ipv6", + }, + AllValid: [][]interface{}{ + { // OpenAPI 3.0: hostname format not define so anything is fine + "notchecked", + }, + { + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:0db8:85a3:0:0:8a2e:0370:7334", + "2001:0db8:85a3::8a2e:0370:7334", + "2001:0db8:85a3::8a2e:370:7334", + "2001:db8::", + "::1234:5678", + "::", + "::1", + "::ffff:0.0.0.0", + "2001:db8:3333:4444:5555:6666:1.2.3.4", + }, }, - AllInvalid: []interface{}{ - 0, - 2, - 4, + AllInvalid: [][]interface{}{ + { + nil, + 3.14, + true, + }, + { + nil, + 3.14, + false, + "", + "56FE::2159:5BBC::6594", // double :: + "a.b.c.d", + "127.0.0.1", + "::192.168.1.y", + // "::192.168.1.02", // fixed in go 1.17 + // ";;192.168.01.2", // fixed in go 1.17 + // "::10.01.2.3", // fixed in go 1.17 + // "::010.1.2.3", // fixed in go 1.17 + "::256.168.1.2", + "::192.256.1.2", + "::192.168.256.2", + "::192.168.1.256", + "::1.2", + "::1.1.1.1.", + "::-1.2.3.4", + "::1...4", + "::1.2..4", + "::1..3.4", + "::1.2.3.4.5", + "::.2.3.4", + "::1:2:3.5:4", + }, }, }, } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 17aad2fa7..7acca8fcd 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -20,6 +20,8 @@ type schemaValidationSettings struct { defaultsSet func() customizeMessageError func(err *SchemaError) string + + openapiMinorVersion uint64 // defaults to 0 (3.0.z) } // FailFast returns schema validation errors quicker. @@ -77,3 +79,8 @@ func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidati } return settings } + +// SetOpenAPIMinorVersion setting allows to define minor OpenAPI version schema must comply with +func SetOpenAPIMinorVersion(minorVersion uint64) SchemaValidationOption { + return func(s *schemaValidationSettings) { s.openapiMinorVersion = minorVersion } +} diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index 813d1c9e8..6b568358e 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -59,6 +59,7 @@ paths: name: id schema: type: string + format: uuid required: true - in: query name: version @@ -92,6 +93,7 @@ components: properties: id: type: string + format: uuid contents: { $ref: '#/components/schemas/TestContents' } required: [id, contents] @@ -114,31 +116,56 @@ components: ` type validatorTestHandler struct { + urlRE *regexp.Regexp contentType string getBody, postBody string errBody string errStatusCode int } -const validatorOkResponse = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}}` +const numValidatorOkResponse = `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}}` +const uuidValidatorOkResponse = `{"id": "D1911DAD-DEBA-4C87-8071-A94BD99D27FC", "contents": {"name": "foo", "expected": 9, "actual": 10}}` -func (h validatorTestHandler) withDefaults() validatorTestHandler { +func (h validatorTestHandler) withNumDefaults() validatorTestHandler { if h.contentType == "" { h.contentType = "application/json" } if h.getBody == "" { - h.getBody = validatorOkResponse + h.getBody = numValidatorOkResponse } if h.postBody == "" { - h.postBody = validatorOkResponse + h.postBody = numValidatorOkResponse } if h.errBody == "" { h.errBody = `{"code":"bad","message":"bad things"}` } + if h.urlRE == nil { + h.urlRE = numUrlRE + } return h } -var testUrlRE = regexp.MustCompile(`^/test(/\d+)?$`) +func (h validatorTestHandler) withUUIDDefaults() validatorTestHandler { + if h.contentType == "" { + h.contentType = "application/json" + } + if h.getBody == "" { + h.getBody = uuidValidatorOkResponse + } + if h.postBody == "" { + h.postBody = uuidValidatorOkResponse + } + if h.errBody == "" { + h.errBody = `{"code":"bad","message":"bad things"}` + } + if h.urlRE == nil { + h.urlRE = uuidUrlRE + } + return h +} + +var numUrlRE = regexp.MustCompile(`^/test(/\d+)?$`) +var uuidUrlRE = regexp.MustCompile(`^/test(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})?$`) func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", h.contentType) @@ -147,7 +174,7 @@ func (h *validatorTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) w.Write([]byte(h.errBody)) return } - if !testUrlRE.MatchString(r.URL.Path) { + if h.urlRE != nil && !h.urlRE.MatchString(r.URL.Path) { w.WriteHeader(http.StatusNotFound) w.Write([]byte(h.errBody)) return @@ -180,26 +207,93 @@ func TestValidator(t *testing.T) { body string } tests := []struct { - name string - handler validatorTestHandler - options []openapi3filter.ValidatorOption - request testRequest - response testResponse - strict bool + name string + openapiVersion openapi3.Version + handler validatorTestHandler + options []openapi3filter.ValidatorOption + request testRequest + response testResponse + strict bool }{{ - name: "valid GET", - handler: validatorTestHandler{}.withDefaults(), + name: "valid num GET 3.0", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "GET", path: "/test/42?version=1", }, response: testResponse{ - 200, validatorOkResponse, + 200, numValidatorOkResponse, + }, + strict: true, + }, { + name: "valid uuid GET 3.0", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withUUIDDefaults(), + request: testRequest{ + method: "GET", + path: "/test/FDB366E3-340F-4DE0-97BD-EF08FB3D4EF7?version=1", + }, + response: testResponse{ + 200, uuidValidatorOkResponse, + }, + strict: true, + }, { + name: "valid uuid GET 3.1", + openapiVersion: "3.1.0", + handler: validatorTestHandler{}.withUUIDDefaults(), + request: testRequest{ + method: "GET", + path: "/test/FDB366E3-340F-4DE0-97BD-EF08FB3D4EF7?version=1", + }, + response: testResponse{ + 200, uuidValidatorOkResponse, + }, + strict: true, + }, { + name: "invalid num GET 3.1; uuid expected", + openapiVersion: "3.1.0", + handler: validatorTestHandler{}.withNumDefaults(), + request: testRequest{ + method: "GET", + path: "/test/42?version=1", + }, + response: testResponse{ + 400, "bad request\n", + }, + strict: true, + }, { + name: "valid num POST 3.0", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 201, numValidatorOkResponse, + }, + strict: true, + }, { + name: "valid uuid POST 3.0", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withUUIDDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 201, uuidValidatorOkResponse, }, strict: true, }, { - name: "valid POST", - handler: validatorTestHandler{}.withDefaults(), + name: "valid uuid POST 3.1", + openapiVersion: "3.1.0", + handler: validatorTestHandler{}.withUUIDDefaults(), request: testRequest{ method: "POST", path: "/test?version=1", @@ -207,12 +301,30 @@ func TestValidator(t *testing.T) { contentType: "application/json", }, response: testResponse{ - 201, validatorOkResponse, + 201, uuidValidatorOkResponse, }, strict: true, }, { - name: "not found; no GET operation for /test", - handler: validatorTestHandler{}.withDefaults(), + name: "invalid response POST 3.1; uuid expected", + openapiVersion: "3.1.0", + handler: validatorTestHandler{ + getBody: numValidatorOkResponse, + postBody: numValidatorOkResponse, + }.withUUIDDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 500, "server error\n", + }, + strict: true, + }, { + name: "not found; no GET operation for /test", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "GET", path: "/test?version=1", @@ -222,8 +334,9 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "not found; no POST operation for /test/42", - handler: validatorTestHandler{}.withDefaults(), + name: "not found; no POST operation for /test/42", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "POST", path: "/test/42?version=1", @@ -233,8 +346,9 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid request; missing version", - handler: validatorTestHandler{}.withDefaults(), + name: "invalid request; missing version", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "GET", path: "/test/42", @@ -244,8 +358,9 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid POST request; wrong property type", - handler: validatorTestHandler{}.withDefaults(), + name: "invalid POST request; wrong property type", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "POST", path: "/test?version=1", @@ -257,8 +372,9 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid POST request; missing property", - handler: validatorTestHandler{}.withDefaults(), + name: "invalid POST request; missing property", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "POST", path: "/test?version=1", @@ -270,8 +386,9 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid POST request; extra property", - handler: validatorTestHandler{}.withDefaults(), + name: "invalid POST request; extra property", + openapiVersion: "3.0.0", + handler: validatorTestHandler{}.withNumDefaults(), request: testRequest{ method: "POST", path: "/test?version=1", @@ -283,12 +400,13 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "valid response; 404 error", + name: "valid response 3.0; 404 error", + openapiVersion: "3.0.0", handler: validatorTestHandler{ contentType: "application/json", errBody: `{"code": "404", "message": "not found"}`, errStatusCode: 404, - }.withDefaults(), + }.withNumDefaults(), request: testRequest{ method: "GET", path: "/test/42?version=1", @@ -298,11 +416,62 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid response; invalid error", + name: "valid response 3.1; 404 error", + openapiVersion: "3.1.0", + handler: validatorTestHandler{ + contentType: "application/json", + errBody: `{"code": "404", "message": "not found"}`, + errStatusCode: 404, + }.withNumDefaults(), + request: testRequest{ + method: "GET", + path: "/test/4506CEF4-FB58-41C7-A5D2-AC538C59F1BA?version=1", + }, + response: testResponse{ + 404, `{"code": "404", "message": "not found"}`, + }, + strict: true, + }, { + name: "invalid response 3.1; uuid expected", + openapiVersion: "3.1.0", + handler: validatorTestHandler{ + getBody: numValidatorOkResponse, + postBody: numValidatorOkResponse, + }.withUUIDDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 500, "server error\n", + }, + strict: true, + }, { + name: "invalid response 3.1; not strict", + openapiVersion: "3.1.0", + handler: validatorTestHandler{ + getBody: numValidatorOkResponse, + postBody: numValidatorOkResponse, + }.withUUIDDefaults(), + request: testRequest{ + method: "POST", + path: "/test?version=1", + body: `{"name": "foo", "expected": 9, "actual": 10}`, + contentType: "application/json", + }, + response: testResponse{ + 201, numValidatorOkResponse, + }, + strict: false, + }, { + name: "invalid response; invalid error", + openapiVersion: "3.0.0", handler: validatorTestHandler{ errBody: `"not found"`, errStatusCode: 404, - }.withDefaults(), + }.withNumDefaults(), request: testRequest{ method: "GET", path: "/test/42?version=1", @@ -312,10 +481,11 @@ func TestValidator(t *testing.T) { }, strict: true, }, { - name: "invalid POST response; not strict", + name: "invalid POST response; not strict", + openapiVersion: "3.0.0", handler: validatorTestHandler{ postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, - }.withDefaults(), + }.withNumDefaults(), request: testRequest{ method: "POST", path: "/test?version=1", @@ -328,12 +498,13 @@ func TestValidator(t *testing.T) { }, strict: false, }, { - name: "POST response status code not in spec (return 200, spec only has 201)", + name: "POST response status code not in spec (return 200, spec only has 201)", + openapiVersion: "3.0.0", handler: validatorTestHandler{ postBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, errStatusCode: 200, errBody: `{"id": "42", "contents": {"name": "foo", "expected": 9, "actual": 10}, "extra": true}`, - }.withDefaults(), + }.withNumDefaults(), options: []openapi3filter.ValidatorOption{openapi3filter.ValidationOptions(openapi3filter.Options{ IncludeResponseStatus: true, })}, @@ -366,6 +537,9 @@ func TestValidator(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err, "failed to validate with test server") + // Define version + doc.OpenAPI = test.openapiVersion + // Create the router and validator router, err := gorillamux.NewRouter(doc) require.NoError(t, err, "failed to create router") diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index f4debcda1..86a586b3a 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -182,6 +182,9 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param if options.customSchemaErrorFunc != nil { opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc)) } + if input.Route != nil { + opts = append(opts, openapi3.SetOpenAPIMinorVersion(input.Route.Spec.OpenAPI.Minor())) + } if err = schema.VisitJSON(value, opts...); err != nil { return &RequestError{Input: input, Parameter: parameter, Err: err} } @@ -286,6 +289,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req if options.ExcludeReadOnlyValidations { opts = append(opts, openapi3.DisableReadOnlyValidation()) } + if input.Route != nil { + opts = append(opts, openapi3.SetOpenAPIMinorVersion(input.Route.Spec.OpenAPI.Minor())) + } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index dca13380a..00fef1205 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -73,6 +73,9 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error if options.ExcludeWriteOnlyValidations { opts = append(opts, openapi3.DisableWriteOnlyValidation()) } + if route != nil { + opts = append(opts, openapi3.SetOpenAPIMinorVersion(route.Spec.OpenAPI.Minor())) + } headers := make([]string, 0, len(response.Headers)) for k := range response.Headers { @@ -148,7 +151,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error } // Validate data with the schema. - if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse())...); err != nil { + if err := contentType.Schema.Value.VisitJSON(value, append(opts, openapi3.VisitAsResponse(), openapi3.SetOpenAPIMinorVersion(route.Spec.OpenAPI.Minor()))...); err != nil { schemaId := getSchemaIdentifier(contentType.Schema) schemaId = prependSpaceIfNeeded(schemaId) return &ResponseError{