Repository: getkin/kin-openapi
Pull request #590 (2022-09-06 13:16:45)

feat: support validation options specifically for disabling pattern validation
addresses: https://github.com/getkin/kin-openapi/issues/409

TristanSpeakEasy: @fenollp thank you for the feedback, very much appreciated. Have addressed all the current comments
TristanSpeakEasy: @fenollp thanks for the feedback. Have addressed your comments. Also recently sent you an email (just in case it got caught up in spam)

Patch:
diff --git a/README.md b/README.md
index a829e59..ae87783 100644
--- a/README.md
+++ b/README.md
@@ -186,6 +186,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool {
 
 ## Sub-v0 breaking API changes
 
+### v0.101.0
+* `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value.
+
 ### v0.84.0
 * The prototype of `openapi3gen.NewSchemaRefForValue` changed:
 	* It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`.
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 806b819..87c9f86 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -166,6 +166,10 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
 
 // ResolveRefsIn expands references if for instance spec was just unmarshalled
 func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
+	if loader.Context == nil {
+		loader.Context = context.Background()
+	}
+
 	if loader.visitedPathItemRefs == nil {
 		loader.resetVisitedPathItemRefs()
 	}
@@ -406,7 +410,6 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef
 		return current
 	}
 	return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)}
-
 }
 
 func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
@@ -837,7 +840,6 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
 }
 
 func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) {
-
 	if component == nil {
 		return errors.New("invalid callback: value MUST be an object")
 	}
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
index cb9183d..20549c2 100644
--- a/openapi3/openapi3.go
+++ b/openapi3/openapi3.go
@@ -54,7 +54,14 @@ func (doc *T) AddServer(server *Server) {
 }
 
 // Validate returns an error if T does not comply with the OpenAPI spec.
-func (doc *T) Validate(ctx context.Context) error {
+// Validations Options can be provided to modify the validation behavior.
+func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
+	validationOpts := &ValidationOptions{}
+	for _, opt := range opts {
+		opt(validationOpts)
+	}
+	ctx = WithValidationOptions(ctx, validationOpts)
+
 	if doc.OpenAPI == "" {
 		return errors.New("value of openapi must be a non-empty string")
 	}
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 17f547e..57f63fb 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -36,9 +36,6 @@ var (
 	// SchemaErrorDetailsDisabled disables printing of details about schema errors.
 	SchemaErrorDetailsDisabled = false
 
-	//SchemaFormatValidationDisabled disables validation of schema type formats.
-	SchemaFormatValidationDisabled = false
-
 	errSchema = errors.New("input does not match the schema")
 
 	// ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema
@@ -403,6 +400,7 @@ func (schema *Schema) WithMax(value float64) *Schema {
 	schema.Max = &value
 	return schema
 }
+
 func (schema *Schema) WithExclusiveMin(value bool) *Schema {
 	schema.ExclusiveMin = value
 	return schema
@@ -606,6 +604,8 @@ func (schema *Schema) Validate(ctx context.Context) error {
 }
 
 func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) {
+	validationOpts := getValidationOptions(ctx)
+
 	for _, existing := range stack {
 		if existing == schema {
 			return
@@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
 			switch format {
 			case "float", "double":
 			default:
-				if !SchemaFormatValidationDisabled {
+				if validationOpts.SchemaFormatValidationEnabled {
 					return unsupportedFormat(format)
 				}
 			}
@@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
 			switch format {
 			case "int32", "int64":
 			default:
-				if !SchemaFormatValidationDisabled {
+				if validationOpts.SchemaFormatValidationEnabled {
 					return unsupportedFormat(format)
 				}
 			}
@@ -698,12 +698,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
 			case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
 			default:
 				// Try to check for custom defined formats
-				if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
+				if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled {
 					return unsupportedFormat(format)
 				}
 			}
 		}
-		if schema.Pattern != "" {
+		if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled {
 			if err = schema.compilePattern(); err != nil {
 				return err
 			}
@@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
 			formatMin = formatMinInt64
 			formatMax = formatMaxInt64
 		default:
-			if !SchemaFormatValidationDisabled {
+			if settings.formatValidationEnabled {
 				return unsupportedFormat(schema.Format)
 			}
 		}
@@ -1237,7 +1237,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 	}
 
 	// "pattern"
-	if schema.Pattern != "" && schema.compiledPattern == nil {
+	if schema.Pattern != "" && schema.compiledPattern == nil && !settings.patternValidationDisabled {
 		var err error
 		if err = schema.compilePattern(); err != nil {
 			if !settings.multiError {
diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go
index cb4c142..854ae84 100644
--- a/openapi3/schema_validation_settings.go
+++ b/openapi3/schema_validation_settings.go
@@ -8,9 +8,11 @@ import (
 type SchemaValidationOption func(*schemaValidationSettings)
 
 type schemaValidationSettings struct {
-	failfast     bool
-	multiError   bool
-	asreq, asrep bool // exclusive (XOR) fields
+	failfast                  bool
+	multiError                bool
+	asreq, asrep              bool // exclusive (XOR) fields
+	formatValidationEnabled   bool
+	patternValidationDisabled bool
 
 	onceSettingDefaults sync.Once
 	defaultsSet         func()
@@ -28,10 +30,21 @@ func MultiErrors() SchemaValidationOption {
 func VisitAsRequest() SchemaValidationOption {
 	return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false }
 }
+
 func VisitAsResponse() SchemaValidationOption {
 	return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true }
 }
 
+// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
+func EnableFormatValidation() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.formatValidationEnabled = true }
+}
+
+// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
+func DisablePatternValidation() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.patternValidationDisabled = true }
+}
+
 // DefaultsSet executes the given callback (once) IFF schema validation set default values.
 func DefaultsSet(f func()) SchemaValidationOption {
 	return func(s *schemaValidationSettings) { s.defaultsSet = f }
diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go
new file mode 100644
index 0000000..f6038ed
--- /dev/null
+++ b/openapi3/validation_options.go
@@ -0,0 +1,40 @@
+package openapi3
+
+import "context"
+
+// ValidationOption allows the modification of how the OpenAPI document is validated.
+type ValidationOption func(options *ValidationOptions)
+
+// ValidationOptions provide configuration for validating OpenAPI documents.
+type ValidationOptions struct {
+	SchemaFormatValidationEnabled   bool
+	SchemaPatternValidationDisabled bool
+}
+
+type validationOptionsKey struct{}
+
+// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
+func EnableSchemaFormatValidation() ValidationOption {
+	return func(options *ValidationOptions) {
+		options.SchemaFormatValidationEnabled = true
+	}
+}
+
+// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
+func DisableSchemaPatternValidation() ValidationOption {
+	return func(options *ValidationOptions) {
+		options.SchemaPatternValidationDisabled = true
+	}
+}
+
+// WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type.
+func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context {
+	return context.WithValue(ctx, validationOptionsKey{}, options)
+}
+
+func getValidationOptions(ctx context.Context) *ValidationOptions {
+	if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok {
+		return options
+	}
+	return &ValidationOptions{}
+}


Test patch:
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 593fa17..4c14dcb 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -1028,7 +1028,10 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) {
 		}
 		for _, typ := range example.AllInvalid {
 			schema := baseSchema.WithFormat(typ)
-			err := schema.Validate(context.Background())
+			ctx := WithValidationOptions(context.Background(), &ValidationOptions{
+				SchemaFormatValidationEnabled: true,
+			})
+			err := schema.Validate(ctx)
 			require.Error(t, err)
 		}
 	}
@@ -1308,6 +1311,6 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) {
 		Type:    "string",
 	}
 
-	var err = schema.Validate(context.Background())
+	err := schema.Validate(context.Background())
 	require.Error(t, err)
 }
diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml
new file mode 100644
index 0000000..8839490
--- /dev/null
+++ b/openapi3/testdata/issue409.yml
@@ -0,0 +1,21 @@
+openapi: 3.0.3
+info:
+  description: Contains Patterns that can't be compiled by the go regexp engine
+  title: Issue 409
+  version: 0.0.1
+paths:
+  /v1/apis/{apiID}:
+    get:
+      description: Get a list of all Apis and there versions for a given workspace
+      operationId: getApisV1
+      parameters:
+        - description: The ID of the API
+          in: path
+          name: apiID
+          required: true
+          schema:
+            type: string
+            pattern: ^[a-zA-Z0-9]{0,4096}$
+      responses:
+        "200":
+          description: OK
diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go
new file mode 100644
index 0000000..561594f
--- /dev/null
+++ b/openapi3/validation_issue409_test.go
@@ -0,0 +1,50 @@
+package openapi3_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func TestIssue409PatternIgnored(t *testing.T) {
+	l := openapi3.NewLoader()
+	s, err := l.LoadFromFile("testdata/issue409.yml")
+	require.NoError(t, err)
+
+	err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
+	assert.NoError(t, err)
+}
+
+func TestIssue409PatternNotIgnored(t *testing.T) {
+	l := openapi3.NewLoader()
+	s, err := l.LoadFromFile("testdata/issue409.yml")
+	require.NoError(t, err)
+
+	err = s.Validate(l.Context)
+	assert.Error(t, err)
+}
+
+func TestIssue409HygienicUseOfCtx(t *testing.T) {
+	l := openapi3.NewLoader()
+	doc, err := l.LoadFromFile("testdata/issue409.yml")
+	require.NoError(t, err)
+
+	err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
+	assert.NoError(t, err)
+	err = doc.Validate(l.Context)
+	assert.Error(t, err)
+
+	// and the other way
+
+	l = openapi3.NewLoader()
+	doc, err = l.LoadFromFile("testdata/issue409.yml")
+	require.NoError(t, err)
+
+	err = doc.Validate(l.Context)
+	assert.Error(t, err)
+	err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation())
+	assert.NoError(t, err)
+}