Repository: getkin/kin-openapi
Pull request #675 (2022-11-21 05:25:35)

fix: wrap the error that came back from the callback (#674)
see #674 and TestFormatCallback_WrapError


Patch:
diff --git a/openapi3/schema.go b/openapi3/schema.go
index e5cded8..1d77e81 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -1298,29 +1298,31 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
 	}
 
 	// "format"
-	var formatErr string
+	var formatStrErr string
+	var formatErr error
 	if format := schema.Format; format != "" {
 		if f, ok := SchemaStringFormats[format]; ok {
 			switch {
 			case f.regexp != nil && f.callback == nil:
 				if cp := f.regexp; !cp.MatchString(value) {
-					formatErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
+					formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
 				}
 			case f.regexp == nil && f.callback != nil:
 				if err := f.callback(value); err != nil {
-					formatErr = err.Error()
+					formatErr = err
 				}
 			default:
-				formatErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
+				formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format)
 			}
 		}
 	}
-	if formatErr != "" {
+	if formatStrErr != "" || formatErr != nil {
 		err := &SchemaError{
 			Value:       value,
 			Schema:      schema,
 			SchemaField: "format",
-			Reason:      formatErr,
+			Reason:      formatStrErr,
+			Origin:      formatErr,
 		}
 		if !settings.multiError {
 			return err
diff --git a/openapi3filter/options.go b/openapi3filter/options.go
index 1622339..14843dd 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -21,4 +21,8 @@ type Options struct {
 
 	// See NoopAuthenticationFunc
 	AuthenticationFunc AuthenticationFunc
+
+	// Indicates whether default values are set in the
+	// request. If true, then they are not set
+	SkipSettingDefaults bool
 }
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index beb47aa..83bea98 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -258,7 +258,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
 	defaultsSet := false
 	opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here
 	opts = append(opts, openapi3.VisitAsRequest())
-	opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
+	if !options.SkipSettingDefaults {
+		opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
+	}
 	if options.MultiError {
 		opts = append(opts, openapi3.MultiErrors())
 	}


Test patch:
diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go
index 5cceb8c..eeb7894 100644
--- a/openapi3/schema_formats_test.go
+++ b/openapi3/schema_formats_test.go
@@ -2,8 +2,10 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -58,3 +60,18 @@ func TestIssue430(t *testing.T) {
 		}
 	}
 }
+
+func TestFormatCallback_WrapError(t *testing.T) {
+	var errSomething = errors.New("something error")
+
+	DefineStringFormatCallback("foobar", func(value string) error {
+		return errSomething
+	})
+
+	s := &Schema{Format: "foobar"}
+	err := s.VisitJSONString("blablabla")
+
+	assert.ErrorIs(t, err, errSomething)
+
+	delete(SchemaStringFormats, "foobar")
+}
diff --git a/openapi3filter/issue639_test.go b/openapi3filter/issue639_test.go
new file mode 100644
index 0000000..2caf1bd
--- /dev/null
+++ b/openapi3filter/issue639_test.go
@@ -0,0 +1,100 @@
+package openapi3filter
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue639(t *testing.T) {
+	loader := openapi3.NewLoader()
+	ctx := loader.Context
+	spec := `
+    openapi: 3.0.0
+    info:
+     version: 1.0.0
+     title: Sample API
+    paths:
+     /items:
+      put:
+        requestBody:
+         content:
+          application/json:
+            schema:
+              properties:
+                testWithdefault:
+                  default: false
+                  type: boolean
+                testNoDefault:
+                  type: boolean
+              type: object
+        responses:
+          '200':
+           description: Successful respons
+`[1:]
+
+	doc, err := loader.LoadFromData([]byte(spec))
+	require.NoError(t, err)
+
+	err = doc.Validate(ctx)
+	require.NoError(t, err)
+
+	router, err := gorillamux.NewRouter(doc)
+	require.NoError(t, err)
+
+	tests := []struct {
+		name               string
+		options            *Options
+		expectedDefaultVal interface{}
+	}{
+		{
+			name: "no defaults are added to requests",
+			options: &Options{
+				SkipSettingDefaults: true,
+			},
+			expectedDefaultVal: nil,
+		},
+
+		{
+			name:               "defaults are added to requests",
+			expectedDefaultVal: false,
+		},
+	}
+
+	for _, testcase := range tests {
+		t.Run(testcase.name, func(t *testing.T) {
+			body := "{\"testNoDefault\": true}"
+			httpReq, err := http.NewRequest(http.MethodPut, "/items", strings.NewReader(body))
+			require.NoError(t, err)
+			httpReq.Header.Set("Content-Type", "application/json")
+			require.NoError(t, err)
+
+			route, pathParams, err := router.FindRoute(httpReq)
+			require.NoError(t, err)
+
+			requestValidationInput := &RequestValidationInput{
+				Request:    httpReq,
+				PathParams: pathParams,
+				Route:      route,
+				Options:    testcase.options,
+			}
+			err = ValidateRequest(ctx, requestValidationInput)
+			require.NoError(t, err)
+			bodyAfterValidation, err := ioutil.ReadAll(httpReq.Body)
+			require.NoError(t, err)
+
+			raw := map[string]interface{}{}
+			err = json.Unmarshal(bodyAfterValidation, &raw)
+			require.NoError(t, err)
+			require.Equal(t, testcase.expectedDefaultVal,
+				raw["testWithdefault"], "default value must not be included")
+		})
+	}
+}