Repository: getkin/kin-openapi
Pull request #757 (2023-01-31 02:18:14)

openapi3: fix resolving Callbacks
Fix for https://github.com/getkin/kin-openapi/issues/753.
And this PR try to fix https://github.com/getkin/kin-openapi/issues/341, is this desired behavior?
 


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 28819d7..3ce7ed9 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -113,6 +113,8 @@ type SchemaRefs []*SchemaRef
 type SchemaValidationOption func(*schemaValidationSettings)
     func DefaultsSet(f func()) SchemaValidationOption
     func DisablePatternValidation() SchemaValidationOption
+    func DisableReadOnlyValidation() SchemaValidationOption
+    func DisableWriteOnlyValidation() SchemaValidationOption
     func EnableFormatValidation() SchemaValidationOption
     func FailFast() SchemaValidationOption
     func MultiErrors() SchemaValidationOption
diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go
index acb83cd..b850653 100644
--- a/openapi3/internalize_refs.go
+++ b/openapi3/internalize_refs.go
@@ -78,11 +78,16 @@ func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolve
 		return false
 	}
 	name := refNameResolver(p.Ref)
-	if _, ok := doc.Components.Parameters[name]; ok {
-		p.Ref = "#/components/parameters/" + name
-		return true
+	if doc.Components != nil {
+		if _, ok := doc.Components.Parameters[name]; ok {
+			p.Ref = "#/components/parameters/" + name
+			return true
+		}
 	}
 
+	if doc.Components == nil {
+		doc.Components = &Components{}
+	}
 	if doc.Components.Parameters == nil {
 		doc.Components.Parameters = make(ParametersMap)
 	}
@@ -96,9 +101,15 @@ func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, par
 		return false
 	}
 	name := refNameResolver(h.Ref)
-	if _, ok := doc.Components.Headers[name]; ok {
-		h.Ref = "#/components/headers/" + name
-		return true
+	if doc.Components != nil {
+		if _, ok := doc.Components.Headers[name]; ok {
+			h.Ref = "#/components/headers/" + name
+			return true
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.Headers == nil {
 		doc.Components.Headers = make(Headers)
@@ -113,9 +124,15 @@ func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameRes
 		return false
 	}
 	name := refNameResolver(r.Ref)
-	if _, ok := doc.Components.RequestBodies[name]; ok {
-		r.Ref = "#/components/requestBodies/" + name
-		return true
+	if doc.Components != nil {
+		if _, ok := doc.Components.RequestBodies[name]; ok {
+			r.Ref = "#/components/requestBodies/" + name
+			return true
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.RequestBodies == nil {
 		doc.Components.RequestBodies = make(RequestBodies)
@@ -130,9 +147,15 @@ func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver,
 		return false
 	}
 	name := refNameResolver(r.Ref)
-	if _, ok := doc.Components.Responses[name]; ok {
-		r.Ref = "#/components/responses/" + name
-		return true
+	if doc.Components != nil {
+		if _, ok := doc.Components.Responses[name]; ok {
+			r.Ref = "#/components/responses/" + name
+			return true
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.Responses == nil {
 		doc.Components.Responses = make(Responses)
@@ -147,9 +170,15 @@ func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver Ref
 		return
 	}
 	name := refNameResolver(ss.Ref)
-	if _, ok := doc.Components.SecuritySchemes[name]; ok {
-		ss.Ref = "#/components/securitySchemes/" + name
-		return
+	if doc.Components != nil {
+		if _, ok := doc.Components.SecuritySchemes[name]; ok {
+			ss.Ref = "#/components/securitySchemes/" + name
+			return
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.SecuritySchemes == nil {
 		doc.Components.SecuritySchemes = make(SecuritySchemes)
@@ -164,9 +193,15 @@ func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, p
 		return
 	}
 	name := refNameResolver(e.Ref)
-	if _, ok := doc.Components.Examples[name]; ok {
-		e.Ref = "#/components/examples/" + name
-		return
+	if doc.Components != nil {
+		if _, ok := doc.Components.Examples[name]; ok {
+			e.Ref = "#/components/examples/" + name
+			return
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.Examples == nil {
 		doc.Components.Examples = make(Examples)
@@ -181,9 +216,15 @@ func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentI
 		return
 	}
 	name := refNameResolver(l.Ref)
-	if _, ok := doc.Components.Links[name]; ok {
-		l.Ref = "#/components/links/" + name
-		return
+	if doc.Components != nil {
+		if _, ok := doc.Components.Links[name]; ok {
+			l.Ref = "#/components/links/" + name
+			return
+		}
+	}
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
 	}
 	if doc.Components.Links == nil {
 		doc.Components.Links = make(Links)
@@ -198,6 +239,10 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver,
 		return false
 	}
 	name := refNameResolver(c.Ref)
+
+	if doc.Components == nil {
+		doc.Components = &Components{}
+	}
 	if doc.Components.Callbacks == nil {
 		doc.Components.Callbacks = make(Callbacks)
 	}
@@ -293,6 +338,9 @@ func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, p
 
 func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) {
 	for _, ops := range paths {
+		if isExternalRef(ops.Ref, parentIsExternal) {
+			parentIsExternal = true
+		}
 		// inline full operations
 		ops.Ref = ""
 
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 72ab8c4..03d45ff 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -44,6 +44,7 @@ type Loader struct {
 
 	visitedDocuments map[string]*T
 
+	visitedCallback       map[*Callback]struct{}
 	visitedExample        map[*Example]struct{}
 	visitedHeader         map[*Header]struct{}
 	visitedLink           map[*Link]struct{}
@@ -243,11 +244,11 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
 	}
 
 	// Visit all operations
-	for entrypoint, pathItem := range doc.Paths {
+	for _, pathItem := range doc.Paths {
 		if pathItem == nil {
 			continue
 		}
-		if err = loader.resolvePathItemRef(doc, entrypoint, pathItem, location); err != nil {
+		if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
 			return
 		}
 	}
@@ -850,6 +851,16 @@ 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 && component.Value != nil {
+		if loader.visitedCallback == nil {
+			loader.visitedCallback = make(map[*Callback]struct{})
+		}
+		if _, ok := loader.visitedCallback[component.Value]; ok {
+			return nil
+		}
+		loader.visitedCallback[component.Value] = struct{}{}
+	}
+
 	if component == nil {
 		return errors.New("invalid callback: value MUST be an object")
 	}
@@ -878,57 +889,8 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
 		return nil
 	}
 
-	for entrypoint, pathItem := range *value {
-		entrypoint, pathItem := entrypoint, pathItem
-		err = func() (err error) {
-			key := "-"
-			if documentPath != nil {
-				key = documentPath.EscapedPath()
-			}
-			key += entrypoint
-			if _, ok := loader.visitedPathItemRefs[key]; ok {
-				return nil
-			}
-			loader.visitedPathItemRefs[key] = struct{}{}
-
-			if pathItem == nil {
-				return errors.New("invalid path item: value MUST be an object")
-			}
-			ref := pathItem.Ref
-			if ref != "" {
-				if isSingleRefElement(ref) {
-					var p PathItem
-					if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
-						return err
-					}
-					*pathItem = p
-				} else {
-					if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil {
-						return
-					}
-
-					rest := strings.TrimPrefix(ref, "#/components/callbacks/")
-					if rest == ref {
-						return fmt.Errorf(`expected prefix "#/components/callbacks/" in URI %q`, ref)
-					}
-					id := unescapeRefString(rest)
-
-					if doc.Components == nil || doc.Components.Callbacks == nil {
-						return failedToResolveRefFragmentPart(ref, "callbacks")
-					}
-					resolved := doc.Components.Callbacks[id]
-					if resolved == nil {
-						return failedToResolveRefFragmentPart(ref, id)
-					}
-
-					for _, p := range *resolved.Value {
-						*pathItem = *p
-						break
-					}
-				}
-			}
-			return loader.resolvePathItemRefContinued(doc, pathItem, documentPath)
-		}()
+	for _, pathItem := range *value {
+		err := loader.resolvePathItemRef(doc, pathItem, documentPath)
 		if err != nil {
 			return err
 		}
@@ -973,22 +935,27 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
 	return nil
 }
 
-func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *PathItem, documentPath *url.URL) (err error) {
-	key := "_"
-	if documentPath != nil {
-		key = documentPath.EscapedPath()
-	}
-	key += entrypoint
-	if _, ok := loader.visitedPathItemRefs[key]; ok {
-		return nil
-	}
-	loader.visitedPathItemRefs[key] = struct{}{}
-
+func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) {
 	if pathItem == nil {
 		return errors.New("invalid path item: value MUST be an object")
 	}
 	ref := pathItem.Ref
 	if ref != "" {
+		if pathItem.Summary != "" ||
+			pathItem.Description != "" ||
+			pathItem.Connect != nil ||
+			pathItem.Delete != nil ||
+			pathItem.Get != nil ||
+			pathItem.Head != nil ||
+			pathItem.Options != nil ||
+			pathItem.Patch != nil ||
+			pathItem.Post != nil ||
+			pathItem.Put != nil ||
+			pathItem.Trace != nil ||
+			len(pathItem.Servers) != 0 ||
+			len(pathItem.Parameters) != 0 {
+			return nil
+		}
 		if isSingleRefElement(ref) {
 			var p PathItem
 			if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
@@ -996,25 +963,14 @@ func (loader *Loader) resolvePathItemRef(doc *T, entrypoint string, pathItem *Pa
 			}
 			*pathItem = p
 		} else {
-			if doc, ref, documentPath, err = loader.resolveRef(doc, ref, documentPath); err != nil {
-				return
-			}
-
-			rest := strings.TrimPrefix(ref, "#/paths/")
-			if rest == ref {
-				return fmt.Errorf(`expected prefix "#/paths/" in URI %q`, ref)
-			}
-			id := unescapeRefString(rest)
-
-			if doc.Paths == nil {
-				return failedToResolveRefFragmentPart(ref, "paths")
-			}
-			resolved := doc.Paths[id]
-			if resolved == nil {
-				return failedToResolveRefFragmentPart(ref, id)
+			var resolved PathItem
+			doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved)
+			if err != nil {
+				return err
 			}
-			*pathItem = *resolved
+			*pathItem = resolved
 		}
+		pathItem.Ref = ref
 	}
 	return loader.resolvePathItemRefContinued(doc, pathItem, documentPath)
 }
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 4e2bf14..5f867ff 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -1787,8 +1787,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 		sort.Strings(properties)
 		for _, propName := range properties {
 			propSchema := schema.Properties[propName]
-			reqRO := settings.asreq && propSchema.Value.ReadOnly
-			repWO := settings.asrep && propSchema.Value.WriteOnly
+			reqRO := settings.asreq && propSchema.Value.ReadOnly && !settings.readOnlyValidationDisabled
+			repWO := settings.asrep && propSchema.Value.WriteOnly && !settings.writeOnlyValidationDisabled
 
 			if value[propName] == nil {
 				if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO {
diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go
index 5a28c8d..17aad2f 100644
--- a/openapi3/schema_validation_settings.go
+++ b/openapi3/schema_validation_settings.go
@@ -8,11 +8,13 @@ import (
 type SchemaValidationOption func(*schemaValidationSettings)
 
 type schemaValidationSettings struct {
-	failfast                  bool
-	multiError                bool
-	asreq, asrep              bool // exclusive (XOR) fields
-	formatValidationEnabled   bool
-	patternValidationDisabled bool
+	failfast                    bool
+	multiError                  bool
+	asreq, asrep                bool // exclusive (XOR) fields
+	formatValidationEnabled     bool
+	patternValidationDisabled   bool
+	readOnlyValidationDisabled  bool
+	writeOnlyValidationDisabled bool
 
 	onceSettingDefaults sync.Once
 	defaultsSet         func()
@@ -47,6 +49,16 @@ func DisablePatternValidation() SchemaValidationOption {
 	return func(s *schemaValidationSettings) { s.patternValidationDisabled = true }
 }
 
+// DisableReadOnlyValidation setting makes Validate not return an error when validating properties marked as read-only
+func DisableReadOnlyValidation() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.readOnlyValidationDisabled = true }
+}
+
+// DisableWriteOnlyValidation setting makes Validate not return an error when validating properties marked as write-only
+func DisableWriteOnlyValidation() SchemaValidationOption {
+	return func(s *schemaValidationSettings) { s.writeOnlyValidationDisabled = 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/openapi3filter/options.go b/openapi3filter/options.go
index 14c35d5..4ea9e99 100644
--- a/openapi3filter/options.go
+++ b/openapi3filter/options.go
@@ -15,6 +15,12 @@ type Options struct {
 	// Set ExcludeResponseBody so ValidateResponse skips response body validation
 	ExcludeResponseBody bool
 
+	// Set ExcludeReadOnlyValidations so ValidateRequest skips read-only validations
+	ExcludeReadOnlyValidations bool
+
+	// Set ExcludeWriteOnlyValidations so ValidateResponse skips write-only validations
+	ExcludeWriteOnlyValidations bool
+
 	// Set IncludeResponseStatus so ValidateResponse fails on response
 	// status not defined in OpenAPI spec
 	IncludeResponseStatus bool
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 7245cbe..a8106a7 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -272,7 +272,7 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
 	}
 
 	defaultsSet := false
-	opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential opts here
+	opts := make([]openapi3.SchemaValidationOption, 0, 4) // 4 potential opts here
 	opts = append(opts, openapi3.VisitAsRequest())
 	if !options.SkipSettingDefaults {
 		opts = append(opts, openapi3.DefaultsSet(func() { defaultsSet = true }))
@@ -283,6 +283,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
 	if options.customSchemaErrorFunc != nil {
 		opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
 	}
+	if options.ExcludeReadOnlyValidations {
+		opts = append(opts, openapi3.DisableReadOnlyValidation())
+	}
 
 	// 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 c1be319..08ea4e1 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -63,13 +63,16 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
 		return &ResponseError{Input: input, Reason: "response has not been resolved"}
 	}
 
-	opts := make([]openapi3.SchemaValidationOption, 0, 2)
+	opts := make([]openapi3.SchemaValidationOption, 0, 3) // 3 potential options here
 	if options.MultiError {
 		opts = append(opts, openapi3.MultiErrors())
 	}
 	if options.customSchemaErrorFunc != nil {
 		opts = append(opts, openapi3.SetSchemaErrorMessageCustomizer(options.customSchemaErrorFunc))
 	}
+	if options.ExcludeWriteOnlyValidations {
+		opts = append(opts, openapi3.DisableWriteOnlyValidation())
+	}
 
 	headers := make([]string, 0, len(response.Headers))
 	for k := range response.Headers {


Test patch:
diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go
index 15ea9d4..ba9bed7 100644
--- a/openapi3/issue341_test.go
+++ b/openapi3/issue341_test.go
@@ -1,6 +1,7 @@
 package openapi3
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -20,7 +21,43 @@ func TestIssue341(t *testing.T) {
 
 	bs, err := doc.MarshalJSON()
 	require.NoError(t, err)
-	require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`, string(bs))
+	require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"$ref":"testpath.yaml#/paths/~1testpath"}}}`, string(bs))
 
 	require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+
+	doc.InternalizeRefs(context.Background(), nil)
+	bs, err = doc.MarshalJSON()
+	require.NoError(t, err)
+	require.JSONEq(t, `{
+		"components": {
+		  "responses": {
+			"testpath_200_response": {
+			  "content": {
+				"application/json": {
+				  "schema": {
+					"type": "string"
+				  }
+				}
+			  },
+			  "description": "a custom response"
+			}
+		  }
+		},
+		"info": {
+		  "title": "test file",
+		  "version": "n/a"
+		},
+		"openapi": "3.0.0",
+		"paths": {
+		  "/testpath": {
+			"get": {
+			  "responses": {
+				"200": {
+				  "$ref": "#/components/responses/testpath_200_response"
+				}
+			  }
+			}
+		  }
+		}
+	  }`, string(bs))
 }
diff --git a/openapi3/issue689_test.go b/openapi3/issue689_test.go
new file mode 100644
index 0000000..cafbadf
--- /dev/null
+++ b/openapi3/issue689_test.go
@@ -0,0 +1,107 @@
+package openapi3_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+)
+
+func TestIssue689(t *testing.T) {
+	t.Parallel()
+
+	tests := [...]struct {
+		name     string
+		schema   *openapi3.Schema
+		value    map[string]interface{}
+		opts     []openapi3.SchemaValidationOption
+		checkErr require.ErrorAssertionFunc
+	}{
+		// read-only
+		{
+			name: "read-only property succeeds when read-only validation is disabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", ReadOnly: true}}),
+			value: map[string]interface{}{"foo": true},
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsRequest(),
+				openapi3.DisableReadOnlyValidation()},
+			checkErr: require.NoError,
+		},
+		{
+			name: "non read-only property succeeds when read-only validation is disabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", ReadOnly: false}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsRequest()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.NoError,
+		},
+		{
+			name: "read-only property fails when read-only validation is enabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", ReadOnly: true}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsRequest()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.Error,
+		},
+		{
+			name: "non read-only property succeeds when read-only validation is enabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", ReadOnly: false}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsRequest()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.NoError,
+		},
+		// write-only
+		{
+			name: "write-only property succeeds when write-only validation is disabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", WriteOnly: true}}),
+			value: map[string]interface{}{"foo": true},
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsResponse(),
+				openapi3.DisableWriteOnlyValidation()},
+			checkErr: require.NoError,
+		},
+		{
+			name: "non write-only property succeeds when write-only validation is disabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", WriteOnly: false}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsResponse()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.NoError,
+		},
+		{
+			name: "write-only property fails when write-only validation is enabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", WriteOnly: true}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsResponse()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.Error,
+		},
+		{
+			name: "non write-only property succeeds when write-only validation is enabled",
+			schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{
+				"foo": {Type: "boolean", WriteOnly: false}}),
+			opts: []openapi3.SchemaValidationOption{
+				openapi3.VisitAsResponse()},
+			value:    map[string]interface{}{"foo": true},
+			checkErr: require.NoError,
+		},
+	}
+
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			t.Parallel()
+			err := test.schema.VisitJSON(test.value, test.opts...)
+			test.checkErr(t, err)
+		})
+	}
+}
diff --git a/openapi3/issue753_test.go b/openapi3/issue753_test.go
new file mode 100644
index 0000000..4390641
--- /dev/null
+++ b/openapi3/issue753_test.go
@@ -0,0 +1,20 @@
+package openapi3
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue753(t *testing.T) {
+	loader := NewLoader()
+
+	doc, err := loader.LoadFromFile("testdata/issue753.yml")
+	require.NoError(t, err)
+
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+
+	require.NotNil(t, (*doc.Paths["/test1"].Post.Callbacks["callback1"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
+	require.NotNil(t, (*doc.Paths["/test2"].Post.Callbacks["callback2"].Value)["{$request.body#/callback}"].Post.RequestBody.Value.Content["application/json"].Schema.Value)
+}
diff --git a/openapi3/testdata/issue753.yml b/openapi3/testdata/issue753.yml
new file mode 100644
index 0000000..2123a6d
--- /dev/null
+++ b/openapi3/testdata/issue753.yml
@@ -0,0 +1,53 @@
+openapi: '3'
+info:
+  version: 0.0.1
+  title: 'test'
+paths:
+  /test1:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+      responses:
+        '200':
+          description: 'test'
+      callbacks:
+        callback1:
+          '{$request.body#/callback}':
+            post:
+              requestBody:
+                content:
+                  application/json:
+                    schema:
+                      $ref: '#/components/schemas/test'
+              responses:
+                '200':
+                  description: 'test'
+  /test2:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+      responses:
+        '200':
+          description: 'test'
+      callbacks:
+        callback2:
+          '{$request.body#/callback}':
+            post:
+              requestBody:
+                content:
+                  application/json:
+                    schema:
+                      $ref: '#/components/schemas/test'
+              responses:
+                '200':
+                  description: 'test'
+components:
+  schemas:
+    test:
+      type: string
diff --git a/openapi3filter/issue689_test.go b/openapi3filter/issue689_test.go
new file mode 100644
index 0000000..592d53f
--- /dev/null
+++ b/openapi3filter/issue689_test.go
@@ -0,0 +1,168 @@
+package openapi3filter
+
+import (
+	"io"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIssue689(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:
+                    testWithReadOnly:
+                      readOnly: true
+                      type: boolean
+                    testNoReadOnly:
+                      type: boolean
+                  type: object
+            responses:
+              '200':
+               description: OK
+          get:
+            responses:
+              '200':
+                description: OK
+                content:
+                  application/json:
+                    schema:
+                      properties:
+                        testWithWriteOnly:
+                          writeOnly: true
+                          type: boolean
+                        testNoWriteOnly:
+                          type: boolean
+`[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
+		body     string
+		method   string
+		checkErr require.ErrorAssertionFunc
+	}{
+		// read-only
+		{
+			name:     "non read-only property is added to request when validation enabled",
+			body:     `{"testNoReadOnly": true}`,
+			method:   http.MethodPut,
+			checkErr: require.NoError,
+		},
+		{
+			name:   "non read-only property is added to request when validation disabled",
+			body:   `{"testNoReadOnly": true}`,
+			method: http.MethodPut,
+			options: &Options{
+				ExcludeReadOnlyValidations: true,
+			},
+			checkErr: require.NoError,
+		},
+		{
+			name:     "read-only property is added to requests when validation enabled",
+			body:     `{"testWithReadOnly": true}`,
+			method:   http.MethodPut,
+			checkErr: require.Error,
+		},
+		{
+			name:   "read-only property is added to requests when validation disabled",
+			body:   `{"testWithReadOnly": true}`,
+			method: http.MethodPut,
+			options: &Options{
+				ExcludeReadOnlyValidations: true,
+			},
+			checkErr: require.NoError,
+		},
+		// write-only
+		{
+			name:     "non write-only property is added to request when validation enabled",
+			body:     `{"testNoWriteOnly": true}`,
+			method:   http.MethodGet,
+			checkErr: require.NoError,
+		},
+		{
+			name:   "non write-only property is added to request when validation disabled",
+			body:   `{"testNoWriteOnly": true}`,
+			method: http.MethodGet,
+			options: &Options{
+				ExcludeWriteOnlyValidations: true,
+			},
+			checkErr: require.NoError,
+		},
+		{
+			name:     "write-only property is added to requests when validation enabled",
+			body:     `{"testWithWriteOnly": true}`,
+			method:   http.MethodGet,
+			checkErr: require.Error,
+		},
+		{
+			name:   "write-only property is added to requests when validation disabled",
+			body:   `{"testWithWriteOnly": true}`,
+			method: http.MethodGet,
+			options: &Options{
+				ExcludeWriteOnlyValidations: true,
+			},
+			checkErr: require.NoError,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			httpReq, err := http.NewRequest(test.method, "/items", strings.NewReader(test.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:    test.options,
+			}
+
+			if test.method == http.MethodGet {
+				responseValidationInput := &ResponseValidationInput{
+					RequestValidationInput: requestValidationInput,
+					Status:                 200,
+					Header:                 httpReq.Header,
+					Body:                   io.NopCloser(strings.NewReader(test.body)),
+					Options:                test.options,
+				}
+				err = ValidateResponse(ctx, responseValidationInput)
+
+			} else {
+				err = ValidateRequest(ctx, requestValidationInput)
+			}
+			test.checkErr(t, err)
+		})
+	}
+}