Repository: getkin/kin-openapi
Pull request #599 (2022-09-17 08:24:28)

readOnly writeOnly validation
Adds support for ``readOnly`` and ``writeOnly`` example validation based on request or response context, if any. Will need to fix relevant tests (or perhaps misunderstood something). Again, all comments appreciated

fenollp: Ah sorry I missed reviewing this and now this needs rebasing.
danicc097: PS: sorry about the useless review code snippets, can't seem to edit the selection after submitting...
danicc097: I think this should cover everything: readOnly and writeOnly properties request, response and examples validation
danicc097: @fenollp does this look good to you, before I fix merge conflicts?

Patch:
diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go
index 4c75e36..fb7a1da 100644
--- a/openapi3/example_validation.go
+++ b/openapi3/example_validation.go
@@ -1,5 +1,16 @@
 package openapi3
 
-func validateExampleValue(input interface{}, schema *Schema) error {
-	return schema.VisitJSON(input, MultiErrors())
+import "context"
+
+func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error {
+	opts := make([]SchemaValidationOption, 0, 2)
+
+	if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
+		opts = append(opts, VisitAsRequest())
+	} else if vo.examplesValidationAsRes {
+		opts = append(opts, VisitAsResponse())
+	}
+	opts = append(opts, MultiErrors())
+
+	return schema.VisitJSON(input, opts...)
 }
diff --git a/openapi3/media_type.go b/openapi3/media_type.go
index 1ae7c99..3500334 100644
--- a/openapi3/media_type.go
+++ b/openapi3/media_type.go
@@ -88,12 +88,12 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
 			return errors.New("example and examples are mutually exclusive")
 		}
 
-		if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
+		if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
 			return nil
 		}
 
 		if example := mediaType.Example; example != nil {
-			if err := validateExampleValue(example, schema.Value); err != nil {
+			if err := validateExampleValue(ctx, example, schema.Value); err != nil {
 				return fmt.Errorf("invalid example: %w", err)
 			}
 		}
@@ -109,7 +109,7 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
 				if err := v.Validate(ctx); err != nil {
 					return fmt.Errorf("example %s: %w", k, err)
 				}
-				if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
+				if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
 					return fmt.Errorf("example %s: %w", k, err)
 				}
 			}
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index 73a39a5..fa07d65 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -318,11 +318,12 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
 		if parameter.Example != nil && parameter.Examples != nil {
 			return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
 		}
-		if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
+
+		if vo := getValidationOptions(ctx); vo.ExamplesValidationDisabled {
 			return nil
 		}
 		if example := parameter.Example; example != nil {
-			if err := validateExampleValue(example, schema.Value); err != nil {
+			if err := validateExampleValue(ctx, example, schema.Value); err != nil {
 				return fmt.Errorf("invalid example: %w", err)
 			}
 		} else if examples := parameter.Examples; examples != nil {
@@ -336,7 +337,7 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
 				if err := v.Validate(ctx); err != nil {
 					return fmt.Errorf("%s: %w", k, err)
 				}
-				if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
+				if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
 					return fmt.Errorf("%s: %w", k, err)
 				}
 			}
diff --git a/openapi3/request_body.go b/openapi3/request_body.go
index c97563a..d28133c 100644
--- a/openapi3/request_body.go
+++ b/openapi3/request_body.go
@@ -109,5 +109,10 @@ func (requestBody *RequestBody) Validate(ctx context.Context) error {
 	if requestBody.Content == nil {
 		return errors.New("content of the request body is required")
 	}
+
+	if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
+		vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
+	}
+
 	return requestBody.Content.Validate(ctx)
 }
diff --git a/openapi3/response.go b/openapi3/response.go
index 62361ad..37325bb 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -115,6 +115,9 @@ func (response *Response) Validate(ctx context.Context) error {
 	if response.Description == nil {
 		return errors.New("a short description of the response is required")
 	}
+	if vo := getValidationOptions(ctx); !vo.ExamplesValidationDisabled {
+		vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
+	}
 
 	if content := response.Content; content != nil {
 		if err := content.Validate(ctx); err != nil {
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 641ce85..596ec04 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"math"
 	"math/big"
+	"reflect"
 	"regexp"
 	"sort"
 	"strconv"
@@ -768,7 +769,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
 	}
 
 	if x := schema.Example; x != nil && !validationOpts.ExamplesValidationDisabled {
-		if err := validateExampleValue(x, schema); err != nil {
+		if err := validateExampleValue(ctx, x, schema); err != nil {
 			return fmt.Errorf("invalid example: %w", err)
 		}
 	}
@@ -870,7 +871,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
 func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, value interface{}) (err error) {
 	if enum := schema.Enum; len(enum) != 0 {
 		for _, v := range enum {
-			if value == v {
+			if reflect.DeepEqual(v, value) {
 				return
 			}
 		}
@@ -1448,6 +1449,8 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
 		return schema.expectedType(settings, TypeObject)
 	}
 
+	var me MultiError
+
 	if settings.asreq || settings.asrep {
 		properties := make([]string, 0, len(schema.Properties))
 		for propName := range schema.Properties {
@@ -1456,19 +1459,28 @@ 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
+
 			if value[propName] == nil {
-				if dlft := propSchema.Value.Default; dlft != nil {
+				if dlft := propSchema.Value.Default; dlft != nil && !reqRO && !repWO {
 					value[propName] = dlft
 					if f := settings.defaultsSet; f != nil {
 						settings.onceSettingDefaults.Do(f)
 					}
 				}
 			}
+
+			if value[propName] != nil {
+				if reqRO {
+					me = append(me, fmt.Errorf("readOnly property %q in request", propName))
+				} else if repWO {
+					me = append(me, fmt.Errorf("writeOnly property %q in response", propName))
+				}
+			}
 		}
 	}
 
-	var me MultiError
-
 	// "properties"
 	properties := schema.Properties
 	lenValue := int64(len(value))
diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go
index 5c0d01d..d890087 100644
--- a/openapi3/validation_options.go
+++ b/openapi3/validation_options.go
@@ -5,11 +5,12 @@ 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.
+// ValidationOptions provides configuration for validating OpenAPI documents.
 type ValidationOptions struct {
-	SchemaFormatValidationEnabled   bool
-	SchemaPatternValidationDisabled bool
-	ExamplesValidationDisabled      bool
+	SchemaFormatValidationEnabled                    bool
+	SchemaPatternValidationDisabled                  bool
+	ExamplesValidationDisabled                       bool
+	examplesValidationAsReq, examplesValidationAsRes bool
 }
 
 type validationOptionsKey struct{}
diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go
index f19123e..ffb7a1f 100644
--- a/openapi3filter/validate_response.go
+++ b/openapi3filter/validate_response.go
@@ -121,7 +121,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
 	}
 
 	opts := make([]openapi3.SchemaValidationOption, 0, 2) // 2 potential opts here
-	opts = append(opts, openapi3.VisitAsRequest())
+	opts = append(opts, openapi3.VisitAsResponse())
 	if options.MultiError {
 		opts = append(opts, openapi3.MultiErrors())
 	}


Test patch:
diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go
index f6a495a..79288c2 100644
--- a/openapi3/example_validation_test.go
+++ b/openapi3/example_validation_test.go
@@ -9,13 +9,16 @@ import (
 
 func TestExamplesSchemaValidation(t *testing.T) {
 	type testCase struct {
-		name                    string
-		requestSchemaExample    string
-		responseSchemaExample   string
-		mediaTypeRequestExample string
-		parametersExample       string
-		componentExamples       string
-		errContains             string
+		name                                  string
+		requestSchemaExample                  string
+		responseSchemaExample                 string
+		mediaTypeRequestExample               string
+		mediaTypeResponseExample              string
+		readWriteOnlyMediaTypeRequestExample  string
+		readWriteOnlyMediaTypeResponseExample string
+		parametersExample                     string
+		componentExamples                     string
+		errContains                           string
 	}
 
 	testCases := []testCase{
@@ -26,7 +29,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
             param1example:
               value: abcd
    `,
-			errContains: "invalid paths: invalid path /user: invalid operation POST: param1example",
+			errContains: `invalid paths: invalid path /user: invalid operation POST: param1example`,
 		},
 		{
 			name: "valid_parameter_examples",
@@ -41,7 +44,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
 			parametersExample: `
           example: abcd
    `,
-			errContains: "invalid path /user: invalid operation POST: invalid example",
+			errContains: `invalid path /user: invalid operation POST: invalid example`,
 		},
 		{
 			name: "valid_parameter_example",
@@ -64,7 +67,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
         email: bad
         password: short
    `,
-			errContains: "invalid paths: invalid path /user: invalid operation POST: example BadUser",
+			errContains: `invalid paths: invalid path /user: invalid operation POST: example BadUser`,
 		},
 		{
 			name: "valid_component_examples",
@@ -90,7 +93,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
               email: bad
               password: short
    `,
-			errContains: "invalid path /user: invalid operation POST: invalid example",
+			errContains: `invalid path /user: invalid operation POST: invalid example`,
 		},
 		{
 			name: "valid_mediatype_examples",
@@ -109,7 +112,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
         email: good@email.com
         # missing password
    `,
-			errContains: "schema \"CreateUserRequest\": invalid example",
+			errContains: `schema "CreateUserRequest": invalid example`,
 		},
 		{
 			name: "valid_schema_request_example",
@@ -127,7 +130,7 @@ func TestExamplesSchemaValidation(t *testing.T) {
         user_id: 1
         # missing access_token
    `,
-			errContains: "schema \"CreateUserResponse\": invalid example",
+			errContains: `schema "CreateUserResponse": invalid example`,
 		},
 		{
 			name: "valid_schema_response_example",
@@ -135,7 +138,64 @@ func TestExamplesSchemaValidation(t *testing.T) {
       example:
         user_id: 1
         access_token: "abcd"
-   `,
+  `,
+		},
+		{
+			name: "valid_readonly_writeonly_examples",
+			readWriteOnlyMediaTypeRequestExample: `
+            examples:
+              ReadWriteOnlyRequest:
+                $ref: '#/components/examples/ReadWriteOnlyRequestData'
+`,
+			readWriteOnlyMediaTypeResponseExample: `
+              examples:
+                ReadWriteOnlyResponse:
+                  $ref: '#/components/examples/ReadWriteOnlyResponseData'
+`,
+			componentExamples: `
+  examples:
+    ReadWriteOnlyRequestData:
+      value:
+        username: user
+        password: password
+    ReadWriteOnlyResponseData:
+      value:
+        user_id: 4321
+  `,
+		},
+		{
+			name: "invalid_readonly_request_examples",
+			readWriteOnlyMediaTypeRequestExample: `
+            examples:
+              ReadWriteOnlyRequest:
+                $ref: '#/components/examples/ReadWriteOnlyRequestData'
+`,
+			componentExamples: `
+  examples:
+    ReadWriteOnlyRequestData:
+      value:
+        username: user
+        password: password
+        user_id: 4321
+`,
+			errContains: `ReadWriteOnlyRequest: readOnly property "user_id" in request`,
+		},
+		{
+			name: "invalid_writeonly_response_examples",
+			readWriteOnlyMediaTypeResponseExample: `
+              examples:
+                ReadWriteOnlyResponse:
+                  $ref: '#/components/examples/ReadWriteOnlyResponseData'
+`,
+			componentExamples: `
+  examples:
+    ReadWriteOnlyResponseData:
+      value:
+        password: password
+        user_id: 4321
+`,
+
+			errContains: `ReadWriteOnlyResponse: writeOnly property "password" in response`,
 		},
 	}
 
@@ -198,7 +258,28 @@ paths:
           content:
             application/json:
               schema:
-                $ref: "#/components/schemas/CreateUserResponse"
+                $ref: "#/components/schemas/CreateUserResponse"`)
+					spec.WriteString(tc.mediaTypeResponseExample)
+					spec.WriteString(`
+  /readWriteOnly:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/ReadWriteOnlyData"
+            required: true`)
+					spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample)
+					spec.WriteString(`
+      responses:
+        '201':
+          description: a response
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/ReadWriteOnlyData"`)
+					spec.WriteString(tc.readWriteOnlyMediaTypeResponseExample)
+					spec.WriteString(`
 components:
   schemas:
     CreateUserRequest:`)
@@ -223,7 +304,6 @@ components:
     CreateUserResponse:`)
 					spec.WriteString(tc.responseSchemaExample)
 					spec.WriteString(`
-      description: represents the response to a User creation
       required:
         - access_token
         - user_id
@@ -234,6 +314,28 @@ components:
           format: int64
           type: integer
       type: object
+    ReadWriteOnlyData:
+      required:
+        # only required in request
+        - username
+        - password
+        # only required in response
+        - user_id
+      properties:
+        username:
+          type: string
+          default: default
+          writeOnly: true # only sent in a request
+        password:
+          type: string
+          default: default
+          writeOnly: true # only sent in a request
+        user_id:
+          format: int64
+          default: 1
+          type: integer
+          readOnly: true # only returned in a response
+      type: object
 `)
 					spec.WriteString(tc.componentExamples)
 
@@ -278,7 +380,7 @@ func TestExampleObjectValidation(t *testing.T) {
               email: real@email.com
               password: validpassword
 `,
-			errContains: "invalid path /user: invalid operation POST: example and examples are mutually exclusive",
+			errContains: `invalid path /user: invalid operation POST: example and examples are mutually exclusive`,
 			componentExamples: `
   examples:
     BadUser:
@@ -295,7 +397,7 @@ func TestExampleObjectValidation(t *testing.T) {
     BadUser:
       description: empty user example
 `,
-			errContains: "invalid components: example \"BadUser\": no value or externalValue field",
+			errContains: `invalid components: example "BadUser": no value or externalValue field`,
 		},
 		{
 			name: "value_externalValue_mutual_exclusion",
@@ -308,7 +410,7 @@ func TestExampleObjectValidation(t *testing.T) {
         password: validpassword
       externalValue: 'http://example.com/examples/example'
 `,
-			errContains: "invalid components: example \"BadUser\": value and externalValue are mutually exclusive",
+			errContains: `invalid components: example "BadUser": value and externalValue are mutually exclusive`,
 		},
 	}
 
diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go
index 4c14dcb..abec304 100644
--- a/openapi3/schema_test.go
+++ b/openapi3/schema_test.go
@@ -1314,3 +1314,45 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) {
 	err := schema.Validate(context.Background())
 	require.Error(t, err)
 }
+
+func TestIssue646(t *testing.T) {
+	data := []byte(`
+enum:
+- 42
+- []
+- [a]
+- {}
+- {b: c}
+`[1:])
+
+	var schema Schema
+	err := yaml.Unmarshal(data, &schema)
+	require.NoError(t, err)
+
+	err = schema.Validate(context.Background())
+	require.NoError(t, err)
+
+	err = schema.VisitJSON(42)
+	require.NoError(t, err)
+
+	err = schema.VisitJSON(1337)
+	require.Error(t, err)
+
+	err = schema.VisitJSON([]interface{}{})
+	require.NoError(t, err)
+
+	err = schema.VisitJSON([]interface{}{"a"})
+	require.NoError(t, err)
+
+	err = schema.VisitJSON([]interface{}{"b"})
+	require.Error(t, err)
+
+	err = schema.VisitJSON(map[string]interface{}{})
+	require.NoError(t, err)
+
+	err = schema.VisitJSON(map[string]interface{}{"b": "c"})
+	require.NoError(t, err)
+
+	err = schema.VisitJSON(map[string]interface{}{"d": "e"})
+	require.Error(t, err)
+}
diff --git a/openapi3filter/validate_readonly_test.go b/openapi3filter/validate_readonly_test.go
index 1152ec8..bad6c96 100644
--- a/openapi3filter/validate_readonly_test.go
+++ b/openapi3filter/validate_readonly_test.go
@@ -2,8 +2,9 @@ package openapi3filter
 
 import (
 	"bytes"
-	"encoding/json"
+	"io"
 	"net/http"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -12,82 +13,218 @@ import (
 	legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
 )
 
-func TestValidatingRequestBodyWithReadOnlyProperty(t *testing.T) {
-	const spec = `{
-  "openapi": "3.0.3",
-  "info": {
-    "version": "1.0.0",
-    "title": "title",
-    "description": "desc",
-    "contact": {
-      "email": "email"
-    }
-  },
-  "paths": {
-    "/accounts": {
-      "post": {
-        "description": "Create a new account",
-        "requestBody": {
-          "required": true,
-          "content": {
-            "application/json": {
-              "schema": {
-                "type": "object",
-                "required": ["_id"],
-                "properties": {
-                  "_id": {
-                    "type": "string",
-                    "description": "Unique identifier for this object.",
-                    "pattern": "[0-9a-v]+$",
-                    "minLength": 20,
-                    "maxLength": 20,
-                    "readOnly": true
-                  }
-                }
-              }
-            }
-          }
-        },
-        "responses": {
-          "201": {
-            "description": "Successfully created a new account"
-          },
-          "400": {
-            "description": "The server could not understand the request due to invalid syntax",
-          }
-        }
-      }
-    }
-  }
-}
-`
+func TestReadOnlyWriteOnlyPropertiesValidation(t *testing.T) {
+	type testCase struct {
+		name                string
+		requestSchema       string
+		responseSchema      string
+		requestBody         string
+		responseBody        string
+		responseErrContains string
+		requestErrContains  string
+	}
 
-	type Request struct {
-		ID string `json:"_id"`
+	testCases := []testCase{
+		{
+			name: "valid_readonly_in_response_and_valid_writeonly_in_request",
+			requestSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["_id"],
+				"properties": {
+					"_id": {
+						"type": "string",
+						"writeOnly": true
+					}
+				}
+			}`,
+			responseSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["access_token"],
+				"properties": {
+					"access_token": {
+						"type": "string",
+						"readOnly": true
+					}
+				}
+			}`,
+			requestBody:  `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
+			responseBody: `{"access_token": "abcd"}`,
+		},
+		{
+			name: "valid_readonly_in_response_and_invalid_readonly_in_request",
+			requestSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["_id"],
+				"properties": {
+					"_id": {
+						"type": "string",
+						"readOnly": true
+					}
+				}
+			}`,
+			responseSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["access_token"],
+				"properties": {
+					"access_token": {
+						"type": "string",
+						"readOnly": true
+					}
+				}
+			}`,
+			requestBody:        `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
+			responseBody:       `{"access_token": "abcd"}`,
+			requestErrContains: `readOnly property "_id" in request`,
+		},
+		{
+			name: "invalid_writeonly_in_response_and_valid_writeonly_in_request",
+			requestSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["_id"],
+				"properties": {
+					"_id": {
+						"type": "string",
+						"writeOnly": true
+					}
+				}
+			}`,
+			responseSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["access_token"],
+				"properties": {
+					"access_token": {
+						"type": "string",
+						"writeOnly": true
+					}
+				}
+			}`,
+			requestBody:         `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
+			responseBody:        `{"access_token": "abcd"}`,
+			responseErrContains: `writeOnly property "access_token" in response`,
+		},
+		{
+			name: "invalid_writeonly_in_response_and_invalid_readonly_in_request",
+			requestSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["_id"],
+				"properties": {
+					"_id": {
+						"type": "string",
+						"readOnly": true
+					}
+				}
+			}`,
+			responseSchema: `
+			"schema":{
+				"type": "object",
+				"required": ["access_token"],
+				"properties": {
+					"access_token": {
+						"type": "string",
+						"writeOnly": true
+					}
+				}
+			}`,
+			requestBody:         `{"_id": "bt6kdc3d0cvp6u8u3ft0"}`,
+			responseBody:        `{"access_token": "abcd"}`,
+			responseErrContains: `writeOnly property "access_token" in response`,
+			requestErrContains:  `readOnly property "_id" in request`,
+		},
 	}
 
-	sl := openapi3.NewLoader()
-	doc, err := sl.LoadFromData([]byte(spec))
-	require.NoError(t, err)
-	err = doc.Validate(sl.Context)
-	require.NoError(t, err)
-	router, err := legacyrouter.NewRouter(doc)
-	require.NoError(t, err)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			spec := bytes.Buffer{}
+			spec.WriteString(`{
+				"openapi": "3.0.3",
+				"info": {
+					"version": "1.0.0",
+					"title": "title"
+				},
+				"paths": {
+					"/accounts": {
+						"post": {
+							"description": "Create a new account",
+							"requestBody": {
+								"required": true,
+								"content": {
+									"application/json": {`)
+			spec.WriteString(tc.requestSchema)
+			spec.WriteString(`}
+								}
+							},
+							"responses": {
+								"201": {
+									"description": "Successfully created a new account",
+									"content": {
+										"application/json": {`)
+			spec.WriteString(tc.responseSchema)
+			spec.WriteString(`}
+									}
+								},
+								"400": {
+									"description": "The server could not understand the request due to invalid syntax",
+								}
+							}
+						}
+					}
+				}
+				}`)
+
+			sl := openapi3.NewLoader()
+			doc, err := sl.LoadFromData(spec.Bytes())
+			require.NoError(t, err)
+			err = doc.Validate(sl.Context)
+			require.NoError(t, err)
+			router, err := legacyrouter.NewRouter(doc)
+			require.NoError(t, err)
+
+			httpReq, err := http.NewRequest(http.MethodPost, "/accounts", strings.NewReader(tc.requestBody))
+			require.NoError(t, err)
+			httpReq.Header.Add(headerCT, "application/json")
 
-	b, err := json.Marshal(Request{ID: "bt6kdc3d0cvp6u8u3ft0"})
-	require.NoError(t, err)
+			route, pathParams, err := router.FindRoute(httpReq)
+			require.NoError(t, err)
 
-	httpReq, err := http.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(b))
-	require.NoError(t, err)
-	httpReq.Header.Add(headerCT, "application/json")
+			reqValidationInput := &RequestValidationInput{
+				Request:    httpReq,
+				PathParams: pathParams,
+				Route:      route,
+			}
 
-	route, pathParams, err := router.FindRoute(httpReq)
-	require.NoError(t, err)
+			if tc.requestSchema != "" {
+				err = ValidateRequest(sl.Context, reqValidationInput)
 
-	err = ValidateRequest(sl.Context, &RequestValidationInput{
-		Request:    httpReq,
-		PathParams: pathParams,
-		Route:      route,
-	})
-	require.NoError(t, err)
+				if tc.requestErrContains != "" {
+					require.Error(t, err)
+					require.Contains(t, err.Error(), tc.requestErrContains)
+				} else {
+					require.NoError(t, err)
+				}
+			}
+
+			if tc.responseSchema != "" {
+				err = ValidateResponse(sl.Context, &ResponseValidationInput{
+					RequestValidationInput: reqValidationInput,
+					Status:                 201,
+					Header:                 httpReq.Header,
+					Body:                   io.NopCloser(strings.NewReader(tc.responseBody)),
+				})
+
+				if tc.responseErrContains != "" {
+					require.Error(t, err)
+					require.Contains(t, err.Error(), tc.responseErrContains)
+				} else {
+					require.NoError(t, err)
+				}
+			}
+		})
+	}
 }
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index b9151b8..c1d4630 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -372,15 +372,6 @@ func getValidationTests(t *testing.T) []*validationTest {
 				Title:  `property "name" is missing`,
 				Source: &ValidationErrorSource{Pointer: "/category/tags/0/name"}},
 		},
-		{
-			// TODO: Add support for validating readonly properties to upstream validator.
-			name: "error - readonly object attribute",
-			args: validationArgs{
-				r: newPetstoreRequest(t, http.MethodPost, "/pet",
-					bytes.NewBufferString(`{"id":213,"name":"Bahama","photoUrls":[]}}`)),
-			},
-			//wantErr: true,
-		},
 		{
 			name: "error - wrong attribute type",
 			args: validationArgs{