Repository: getkin/kin-openapi (go)

Issue:
Examples not being validated
As per #526 I'd expect schema `examples` to be validated as well, but I can't get these tests to fail validation using either `examples` or `example` (this one not implemented afaik). Am I missing something in the code below?

```go
package openapi3

import (
	"testing"

	"github.com/stretchr/testify/require"
)

func TestExamplesValidation(t *testing.T) {
	spec := []byte(`
openapi: 3.0.0

paths:
  /user:
    post:
      description: User creation.
      operationId: createUser
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
            examples:
              BadUser:
                $ref: '#/components/examples/BadUser'
        description: Created user object
        required: true
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateUserResponse"
          description: successful operation
      # security:
      #   - api_key: []
      summary: creates a new user
      tags:
        - user
components:
  schemas:
    CreateUserRequest:
      description: represents a new User
      example:
        username: "]bad["
        email: bad
        password: short
      required:
        - username
        - email
        - password
      properties:
        username:
          type: string
          pattern: "^[ a-zA-Z0-9_-]+$"
          minLength: 3
        email:
          type: string
          pattern: "^[A-Za-z0-9+_.-]+@(.+)$"
        password:
          type: string
          minLength: 7
      title: a User
      type: object
    CreateUserResponse:
      description: represents the response to a User creation
      properties:
        access_token:
          type: string
        user_id:
          format: int64
          type: integer
      type: object
# implemented in #526
  examples:
    BadUser:
      value:
        username: "]bad["
        email: bad
        password: short
info:
  title: An API
  version: 1.2.3.4
`)

	loader := NewLoader()

	doc, err := loader.LoadFromData(spec)
	require.NoError(t, err)

	err = doc.Validate(loader.Context)
	require.Error(t, err) // FAIL
}
```


**EDIT**: well this explains it:

```go
// Validate returns an error if Example does not comply with the OpenAPI spec.
func (example *Example) Validate(ctx context.Context) error {
	return nil // TODO
}
```

Is this being worked on/planned/would accept a PR for it?


Pull request description:
Examples validation
First draft to fix #588. Let me know what you think @fenollp before digging more into it and having proper tests.

Should this be non-configurable? Based on the spec: 

> Tooling implementations MAY choose to validate compatibility automatically, and reject the example value(s) if incompatible.`

Patch:
diff --git a/openapi3/components.go b/openapi3/components.go
index 2f19943..e9af269 100644
--- a/openapi3/components.go
+++ b/openapi3/components.go
@@ -99,7 +99,7 @@ func (components *Components) Validate(ctx context.Context) (err error) {
 			return
 		}
 		if err = v.Validate(ctx); err != nil {
-			return
+			return fmt.Errorf("%s: %s", k, err)
 		}
 	}
 
diff --git a/openapi3/example.go b/openapi3/example.go
index ee40d9e..e63c78f 100644
--- a/openapi3/example.go
+++ b/openapi3/example.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	"github.com/go-openapi/jsonpointer"
@@ -55,5 +56,12 @@ func (example *Example) UnmarshalJSON(data []byte) error {
 
 // Validate returns an error if Example does not comply with the OpenAPI spec.
 func (example *Example) Validate(ctx context.Context) error {
-	return nil // TODO
+	if example.Value != nil && example.ExternalValue != "" {
+		return errors.New("value and externalValue are mutually exclusive")
+	}
+	if example.Value == nil && example.ExternalValue == "" {
+		return errors.New("example has no value or externalValue field")
+	}
+
+	return nil
 }
diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go
new file mode 100644
index 0000000..4c75e36
--- /dev/null
+++ b/openapi3/example_validation.go
@@ -0,0 +1,5 @@
+package openapi3
+
+func validateExampleValue(input interface{}, schema *Schema) error {
+	return schema.VisitJSON(input, MultiErrors())
+}
diff --git a/openapi3/media_type.go b/openapi3/media_type.go
index fc95244..01df12a 100644
--- a/openapi3/media_type.go
+++ b/openapi3/media_type.go
@@ -2,6 +2,8 @@ package openapi3
 
 import (
 	"context"
+	"errors"
+	"fmt"
 
 	"github.com/go-openapi/jsonpointer"
 
@@ -80,7 +82,28 @@ func (mediaType *MediaType) Validate(ctx context.Context) error {
 		if err := schema.Validate(ctx); err != nil {
 			return err
 		}
+		if mediaType.Example != nil && mediaType.Examples != nil {
+			return errors.New("example and examples are mutually exclusive")
+		}
+		if validationOpts := getValidationOptions(ctx); validationOpts.ExamplesValidationDisabled {
+			return nil
+		}
+		if example := mediaType.Example; example != nil {
+			if err := validateExampleValue(example, schema.Value); err != nil {
+				return err
+			}
+		} else if examples := mediaType.Examples; examples != nil {
+			for k, v := range examples {
+				if err := v.Validate(ctx); err != nil {
+					return fmt.Errorf("%s: %s", k, err)
+				}
+				if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
+					return fmt.Errorf("%s: %s", k, err)
+				}
+			}
+		}
 	}
+
 	return nil
 }
 
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index 7783484..f5d7d1f 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -303,16 +303,38 @@ func (parameter *Parameter) Validate(ctx context.Context) error {
 		e := errors.New("parameter must contain exactly one of content and schema")
 		return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, e)
 	}
-	if schema := parameter.Schema; schema != nil {
-		if err := schema.Validate(ctx); err != nil {
-			return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err)
-		}
-	}
 
 	if content := parameter.Content; content != nil {
 		if err := content.Validate(ctx); err != nil {
 			return fmt.Errorf("parameter %q content is invalid: %v", parameter.Name, err)
 		}
 	}
+
+	if schema := parameter.Schema; schema != nil {
+		if err := schema.Validate(ctx); err != nil {
+			return fmt.Errorf("parameter %q schema is invalid: %v", parameter.Name, err)
+		}
+		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 {
+			return nil
+		}
+		if example := parameter.Example; example != nil {
+			if err := validateExampleValue(example, schema.Value); err != nil {
+				return err
+			}
+		} else if examples := parameter.Examples; examples != nil {
+			for k, v := range examples {
+				if err := v.Validate(ctx); err != nil {
+					return fmt.Errorf("%s: %s", k, err)
+				}
+				if err := validateExampleValue(v.Value.Value, schema.Value); err != nil {
+					return fmt.Errorf("%s: %s", k, err)
+				}
+			}
+		}
+	}
+
 	return nil
 }
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 57f63fb..c59070e 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -753,6 +753,12 @@ 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 {
+			return fmt.Errorf("invalid schema example: %s", err)
+		}
+	}
+
 	return
 }
 
diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go
index f6038ed..5c0d01d 100644
--- a/openapi3/validation_options.go
+++ b/openapi3/validation_options.go
@@ -9,6 +9,7 @@ type ValidationOption func(options *ValidationOptions)
 type ValidationOptions struct {
 	SchemaFormatValidationEnabled   bool
 	SchemaPatternValidationDisabled bool
+	ExamplesValidationDisabled      bool
 }
 
 type validationOptionsKey struct{}
@@ -27,6 +28,13 @@ func DisableSchemaPatternValidation() ValidationOption {
 	}
 }
 
+// DisableExamplesValidation disables all example schema validation.
+func DisableExamplesValidation() ValidationOption {
+	return func(options *ValidationOptions) {
+		options.ExamplesValidationDisabled = 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)


Test patch:
diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go
new file mode 100644
index 0000000..85e158e
--- /dev/null
+++ b/openapi3/example_validation_test.go
@@ -0,0 +1,426 @@
+package openapi3
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestExamplesSchemaValidation(t *testing.T) {
+	type testCase struct {
+		name                    string
+		requestSchemaExample    string
+		responseSchemaExample   string
+		mediaTypeRequestExample string
+		parametersExample       string
+		componentExamples       string
+		errContains             string
+	}
+
+	testCases := []testCase{
+		{
+			name: "invalid_parameter_examples",
+			parametersExample: `
+          examples:
+            param1example:
+              value: abcd
+   `,
+			errContains: "invalid paths: param1example",
+		},
+		{
+			name: "valid_parameter_examples",
+			parametersExample: `
+          examples:
+            param1example:
+              value: 1
+   `,
+		},
+		{
+			name: "invalid_parameter_example",
+			parametersExample: `
+          example: abcd
+   `,
+			errContains: "invalid paths",
+		},
+		{
+			name: "valid_parameter_example",
+			parametersExample: `
+          example: 1
+   `,
+		},
+		{
+			name: "invalid_component_examples",
+			mediaTypeRequestExample: `
+            examples:
+              BadUser:
+                $ref: '#/components/examples/BadUser'
+   `,
+			componentExamples: `
+  examples:
+    BadUser:
+      value:
+        username: "]bad["
+        email: bad
+        password: short
+   `,
+			errContains: "invalid paths: BadUser",
+		},
+		{
+			name: "valid_component_examples",
+			mediaTypeRequestExample: `
+            examples:
+              BadUser:
+                $ref: '#/components/examples/BadUser'
+   `,
+			componentExamples: `
+  examples:
+    BadUser:
+      value:
+        username: good
+        email: good@mail.com
+        password: password
+   `,
+		},
+		{
+			name: "invalid_mediatype_examples",
+			mediaTypeRequestExample: `
+            example:
+              username: "]bad["
+              email: bad
+              password: short
+   `,
+			errContains: "invalid paths",
+		},
+		{
+			name: "valid_mediatype_examples",
+			mediaTypeRequestExample: `
+            example:
+              username: good
+              email: good@mail.com
+              password: password
+   `,
+		},
+		{
+			name: "invalid_schema_request_example",
+			requestSchemaExample: `
+      example:
+        username: good
+        email: good@email.com
+        # missing password
+   `,
+			errContains: "invalid schema example",
+		},
+		{
+			name: "valid_schema_request_example",
+			requestSchemaExample: `
+      example:
+        username: good
+        email: good@email.com
+        password: password
+   `,
+		},
+		{
+			name: "invalid_schema_response_example",
+			responseSchemaExample: `
+      example:
+        user_id: 1
+        # missing access_token
+   `,
+			errContains: "invalid schema example",
+		},
+		{
+			name: "valid_schema_response_example",
+			responseSchemaExample: `
+      example:
+        user_id: 1
+        access_token: "abcd"
+   `,
+		},
+	}
+
+	testOptions := []struct {
+		name                      string
+		disableExamplesValidation bool
+	}{
+		{
+			name:                      "examples_validation_disabled",
+			disableExamplesValidation: true,
+		},
+		{
+			name:                      "examples_validation_enabled",
+			disableExamplesValidation: false,
+		},
+	}
+
+	t.Parallel()
+
+	for _, testOption := range testOptions {
+		testOption := testOption
+		t.Run(testOption.name, func(t *testing.T) {
+			t.Parallel()
+			for _, tc := range testCases {
+				t.Run(tc.name, func(t *testing.T) {
+					loader := NewLoader()
+
+					spec := bytes.Buffer{}
+					spec.WriteString(`
+openapi: 3.0.3
+info:
+  title: An API
+  version: 1.2.3.4
+paths:
+  /user:
+    post:
+      description: User creation.
+      operationId: createUser
+      parameters:
+        - name: param1
+          in: 'query'
+          schema:
+            format: int64
+            type: integer`)
+					spec.WriteString(tc.parametersExample)
+					spec.WriteString(`
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/CreateUserRequest"
+`)
+					spec.WriteString(tc.mediaTypeRequestExample)
+					spec.WriteString(`
+        description: Created user object
+        required: true
+      responses:
+        '204':
+          description: "success"
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/CreateUserResponse"
+components:
+  schemas:
+    CreateUserRequest:`)
+					spec.WriteString(tc.requestSchemaExample)
+					spec.WriteString(`
+      required:
+        - username
+        - email
+        - password
+      properties:
+        username:
+          type: string
+          pattern: "^[ a-zA-Z0-9_-]+$"
+          minLength: 3
+        email:
+          type: string
+          pattern: "^[A-Za-z0-9+_.-]+@(.+)$"
+        password:
+          type: string
+          minLength: 7
+      type: object
+    CreateUserResponse:`)
+					spec.WriteString(tc.responseSchemaExample)
+					spec.WriteString(`
+      description: represents the response to a User creation
+      required:
+        - access_token
+        - user_id
+      properties:
+        access_token:
+          type: string
+        user_id:
+          format: int64
+          type: integer
+      type: object
+`)
+					spec.WriteString(tc.componentExamples)
+
+					doc, err := loader.LoadFromData(spec.Bytes())
+					require.NoError(t, err)
+
+					if testOption.disableExamplesValidation {
+						err = doc.Validate(loader.Context, DisableExamplesValidation())
+					} else {
+						err = doc.Validate(loader.Context)
+					}
+
+					if tc.errContains != "" && !testOption.disableExamplesValidation {
+						require.Error(t, err)
+						require.Contains(t, err.Error(), tc.errContains)
+					} else {
+						require.NoError(t, err)
+					}
+				})
+			}
+		})
+	}
+}
+
+func TestExampleObjectValidation(t *testing.T) {
+	type testCase struct {
+		name                    string
+		mediaTypeRequestExample string
+		componentExamples       string
+		errContains             string
+	}
+
+	testCases := []testCase{
+		{
+			name: "example_examples_mutually_exclusive",
+			mediaTypeRequestExample: `
+            examples:
+              BadUser:
+                $ref: '#/components/examples/BadUser'
+            example:
+              username: good
+              email: real@email.com
+              password: validpassword
+`,
+			errContains: "example and examples are mutually exclusive",
+			componentExamples: `
+  examples:
+    BadUser:
+      value:
+        username: "]bad["
+        email: bad
+        password: short
+`,
+		},
+		{
+			name: "example_without_value",
+			componentExamples: `
+  examples:
+    BadUser:
+      description: empty user example
+`,
+			errContains: "example has no value or externalValue field",
+		},
+		{
+			name: "value_externalValue_mutual_exclusion",
+			componentExamples: `
+  examples:
+    BadUser:
+      value:
+        username: good
+        email: real@email.com
+        password: validpassword
+      externalValue: 'http://example.com/examples/example'
+`,
+			errContains: "value and externalValue are mutually exclusive",
+		},
+	}
+
+	testOptions := []struct {
+		name                      string
+		disableExamplesValidation bool
+	}{
+		{
+			name:                      "examples_validation_disabled",
+			disableExamplesValidation: true,
+		},
+		{
+			name:                      "examples_validation_enabled",
+			disableExamplesValidation: false,
+		},
+	}
+
+	t.Parallel()
+
+	for _, testOption := range testOptions {
+		testOption := testOption
+		t.Run(testOption.name, func(t *testing.T) {
+			t.Parallel()
+			for _, tc := range testCases {
+				t.Run(tc.name, func(t *testing.T) {
+					loader := NewLoader()
+
+					spec := bytes.Buffer{}
+					spec.WriteString(`
+openapi: 3.0.3
+info:
+  title: An API
+  version: 1.2.3.4
+paths:
+  /user:
+    post:
+      description: User creation.
+      operationId: createUser
+      parameters:
+        - name: param1
+          in: 'query'
+          schema:
+            format: int64
+            type: integer
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/CreateUserRequest"
+`)
+					spec.WriteString(tc.mediaTypeRequestExample)
+					spec.WriteString(`
+        description: Created user object
+        required: true
+      responses:
+        '204':
+          description: "success"
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/CreateUserResponse"
+components:
+  schemas:
+    CreateUserRequest:
+      required:
+        - username
+        - email
+        - password
+      properties:
+        username:
+          type: string
+          pattern: "^[ a-zA-Z0-9_-]+$"
+          minLength: 3
+        email:
+          type: string
+          pattern: "^[A-Za-z0-9+_.-]+@(.+)$"
+        password:
+          type: string
+          minLength: 7
+      type: object
+    CreateUserResponse:
+      description: represents the response to a User creation
+      required:
+        - access_token
+        - user_id
+      properties:
+        access_token:
+          type: string
+        user_id:
+          format: int64
+          type: integer
+      type: object
+`)
+					spec.WriteString(tc.componentExamples)
+
+					doc, err := loader.LoadFromData(spec.Bytes())
+					require.NoError(t, err)
+
+					if testOption.disableExamplesValidation {
+						err = doc.Validate(loader.Context, DisableExamplesValidation())
+					} else {
+						err = doc.Validate(loader.Context)
+					}
+
+					if tc.errContains != "" {
+						require.Error(t, err)
+						require.Contains(t, err.Error(), tc.errContains)
+					} else {
+						require.NoError(t, err)
+					}
+				})
+			}
+		})
+	}
+}