Repository: getkin/kin-openapi
Pull request #114 (2019-09-06 05:59:54)

Validate additional OpenAPI objects
Hello! I'm incorporating this library into an OpenAPI validation and management service and have noticed that the included validation functions don't check the `openapi` version object (i.e. it can be omitted and validation succeeds).

I've made the following changes in an attempt to address this:

- Adds validation to the `openapi` object
- Removes redundant validation on the `paths` object
- Annotates validation errors in nested objects

@nogates

fenollp: Amazing!
Could you add tests for the error cases you're adding support for?
Also, why make `info` `omitempty`-able?
disposedtrolley: > Could you add tests for the error cases you're adding support for?
Yup sure thing!

> Also, why make `info` `omitempty`-able?
Must've been an oversight on my part. I saw that all of the properties within the Info struct were optional, so I made the entire thing optional. I've just reviewed the [OpenAPI spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#info-object) and I can see that the `title` and `version` properties of the `info` object are actually mandatory, so I'll make the changes to align to that.
disposedtrolley: @fenollp I've added tests for the new errors we're returning and also have added validation to some more objects. I think it might be ready to be merged! Let me know if I should add anything else

Patch:
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 7fb80a6..ebe1be9 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -129,7 +129,7 @@ type Operation struct {
 	Tags         []string               `json:"tags,omitempty"`
 	OperationID  string                 `json:"operationId,omitempty"`
 	Parameters   Parameters             `json:"parameters,omitempty"`
-	Responses    map[string]*Response   `json:"responses,omitempty"`
+	Responses    map[string]*Response   `json:"responses"`
 	Consumes     []string               `json:"consumes,omitempty"`
 	Produces     []string               `json:"produces,omitempty"`
 	Security     *SecurityRequirements  `json:"security,omitempty"`
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index addd172..d9395e8 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -13,7 +13,7 @@ import (
 func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 	result := &openapi3.Swagger{
 		OpenAPI: "3.0",
-		Info:    swagger.Info,
+		Info:    &swagger.Info,
 		Components: openapi3.Components{
 			Tags: swagger.Tags,
 		},
@@ -272,7 +272,7 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
 
 func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 	result := &openapi2.Swagger{
-		Info: swagger.Info,
+		Info: *swagger.Info,
 		Tags: swagger.Components.Tags,
 	}
 	isHTTPS := false
diff --git a/openapi3/info.go b/openapi3/info.go
index 3cff330..8f905f8 100644
--- a/openapi3/info.go
+++ b/openapi3/info.go
@@ -1,18 +1,22 @@
 package openapi3
 
 import (
+	"context"
+	"errors"
+	"fmt"
+
 	"github.com/getkin/kin-openapi/jsoninfo"
 )
 
 // Info is specified by OpenAPI/Swagger standard version 3.0.
 type Info struct {
 	ExtensionProps
-	Title          string   `json:"title,omitempty"`
+	Title          string   `json:"title"` // Required
 	Description    string   `json:"description,omitempty"`
 	TermsOfService string   `json:"termsOfService,omitempty"`
 	Contact        *Contact `json:"contact,omitempty"`
 	License        *License `json:"license,omitempty"`
-	Version        string   `json:"version,omitempty"`
+	Version        string   `json:"version"` // Required
 }
 
 func (value *Info) MarshalJSON() ([]byte, error) {
@@ -23,6 +27,30 @@ func (value *Info) UnmarshalJSON(data []byte) error {
 	return jsoninfo.UnmarshalStrictStruct(data, value)
 }
 
+func (value *Info) Validate(c context.Context) error {
+	if contact := value.Contact; contact != nil {
+		if err := contact.Validate(c); err != nil {
+			return fmt.Errorf("Error when validating Contact: %s", err.Error())
+		}
+	}
+
+	if license := value.License; license != nil {
+		if err := license.Validate(c); err != nil {
+			return fmt.Errorf("Error when validating License: %s", err.Error())
+		}
+	}
+
+	if value.Version == "" {
+		return fmt.Errorf("Variable 'version' must be a non-empty JSON string")
+	}
+
+	if value.Title == "" {
+		return fmt.Errorf("Variable 'title' must be a non-empty JSON string")
+	}
+
+	return nil
+}
+
 // Contact is specified by OpenAPI/Swagger standard version 3.0.
 type Contact struct {
 	ExtensionProps
@@ -39,10 +67,14 @@ func (value *Contact) UnmarshalJSON(data []byte) error {
 	return jsoninfo.UnmarshalStrictStruct(data, value)
 }
 
+func (value *Contact) Validate(c context.Context) error {
+	return nil
+}
+
 // License is specified by OpenAPI/Swagger standard version 3.0.
 type License struct {
 	ExtensionProps
-	Name string `json:"name,omitempty"`
+	Name string `json:"name"` // Required
 	URL  string `json:"url,omitempty"`
 }
 
@@ -53,3 +85,10 @@ func (value *License) MarshalJSON() ([]byte, error) {
 func (value *License) UnmarshalJSON(data []byte) error {
 	return jsoninfo.UnmarshalStrictStruct(data, value)
 }
+
+func (value *License) Validate(c context.Context) error {
+	if value.Name == "" {
+		return errors.New("Variable 'name' must be a non-empty JSON string")
+	}
+	return nil
+}
diff --git a/openapi3/operation.go b/openapi3/operation.go
index eb2aa6f..d33121d 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"fmt"
 	"strconv"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
@@ -29,8 +30,8 @@ type Operation struct {
 	// Optional body parameter.
 	RequestBody *RequestBodyRef `json:"requestBody,omitempty"`
 
-	// Optional responses.
-	Responses Responses `json:"responses,omitempty"`
+	// Responses.
+	Responses Responses `json:"responses"` // Required
 
 	// Optional callbacks
 	Callbacks map[string]*CallbackRef `json:"callbacks,omitempty"`
@@ -94,6 +95,8 @@ func (operation *Operation) Validate(c context.Context) error {
 		if err := v.Validate(c); err != nil {
 			return err
 		}
+	} else {
+		return fmt.Errorf("Variable 'Responses' must be a JSON object")
 	}
 	return nil
 }
diff --git a/openapi3/path_item.go b/openapi3/path_item.go
index fe4ac47..0ac0584 100644
--- a/openapi3/path_item.go
+++ b/openapi3/path_item.go
@@ -22,6 +22,7 @@ type PathItem struct {
 	Trace       *Operation `json:"trace,omitempty"`
 	Servers     Servers    `json:"servers,omitempty"`
 	Parameters  Parameters `json:"parameters,omitempty"`
+	Ref         string     `json:"$ref,omitempty"`
 }
 
 func (pathItem *PathItem) MarshalJSON() ([]byte, error) {
diff --git a/openapi3/server.go b/openapi3/server.go
index 7d2ace5..9434397 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -3,6 +3,7 @@ package openapi3
 import (
 	"context"
 	"errors"
+	"fmt"
 	"net/url"
 	"strings"
 )
@@ -35,7 +36,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string)
 
 // Server is specified by OpenAPI/Swagger standard version 3.0.
 type Server struct {
-	URL         string                     `json:"url,omitempty"`
+	URL         string                     `json:"url"`
 	Description string                     `json:"description,omitempty"`
 	Variables   map[string]*ServerVariable `json:"variables,omitempty"`
 }
@@ -100,6 +101,9 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) {
 }
 
 func (server *Server) Validate(c context.Context) (err error) {
+	if server.URL == "" {
+		return fmt.Errorf("Variable 'URL' must be a non-empty JSON string")
+	}
 	for _, v := range server.Variables {
 		if err = v.Validate(c); err != nil {
 			return
diff --git a/openapi3/swagger.go b/openapi3/swagger.go
index 3f02de3..5442fd4 100644
--- a/openapi3/swagger.go
+++ b/openapi3/swagger.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/getkin/kin-openapi/jsoninfo"
 )
@@ -9,9 +10,9 @@ import (
 type Swagger struct {
 	ExtensionProps
 	OpenAPI      string               `json:"openapi"` // Required
-	Info         Info                 `json:"info"`    // Required
+	Info         *Info                `json:"info"`    // Required
 	Servers      Servers              `json:"servers,omitempty"`
-	Paths        Paths                `json:"paths,omitempty"`
+	Paths        Paths                `json:"paths"` // Required
 	Components   Components           `json:"components,omitempty"`
 	Security     SecurityRequirements `json:"security,omitempty"`
 	ExternalDocs *ExternalDocs        `json:"externalDocs,omitempty"`
@@ -44,28 +45,35 @@ func (swagger *Swagger) AddServer(server *Server) {
 }
 
 func (swagger *Swagger) Validate(c context.Context) error {
+	if swagger.OpenAPI == "" {
+		return fmt.Errorf("Variable 'openapi' must be a non-empty JSON string")
+	}
 	if err := swagger.Components.Validate(c); err != nil {
-		return err
+		return fmt.Errorf("Error when validating Components: %s", err.Error())
 	}
 	if v := swagger.Security; v != nil {
 		if err := v.Validate(c); err != nil {
-			return err
-		}
-	}
-	if paths := swagger.Paths; paths != nil {
-		if err := paths.Validate(c); err != nil {
-			return err
+			return fmt.Errorf("Error when validating Security: %s", err.Error())
 		}
 	}
 	if v := swagger.Servers; v != nil {
 		if err := v.Validate(c); err != nil {
-			return err
+			return fmt.Errorf("Error when validating Servers: %s", err.Error())
 		}
 	}
 	if v := swagger.Paths; v != nil {
 		if err := v.Validate(c); err != nil {
-			return err
+			return fmt.Errorf("Error when validating Paths: %s", err.Error())
+		}
+	} else {
+		return fmt.Errorf("Variable 'paths' must be a JSON object")
+	}
+	if v := swagger.Info; v != nil {
+		if err := v.Validate(c); err != nil {
+			return fmt.Errorf("Error when validating Info: %s", err.Error())
 		}
+	} else {
+		return fmt.Errorf("Variable 'info' must be a JSON object")
 	}
 	return nil
 }
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
index d667462..965a217 100644
--- a/openapi3/swagger_loader.go
+++ b/openapi3/swagger_loader.go
@@ -30,13 +30,23 @@ type SwaggerLoader struct {
 	Context                context.Context
 	LoadSwaggerFromURIFunc func(loader *SwaggerLoader, url *url.URL) (*Swagger, error)
 	visited                map[interface{}]struct{}
+	visitedFiles           map[string]struct{}
 }
 
 func NewSwaggerLoader() *SwaggerLoader {
 	return &SwaggerLoader{}
 }
 
+func (swaggerLoader *SwaggerLoader) reset() {
+	swaggerLoader.visitedFiles = make(map[string]struct{})
+}
+
 func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swagger, error) {
+	swaggerLoader.reset()
+	return swaggerLoader.loadSwaggerFromURIInternal(location)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) {
 	f := swaggerLoader.LoadSwaggerFromURIFunc
 	if f != nil {
 		return f(swaggerLoader, location)
@@ -45,7 +55,7 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swag
 	if err != nil {
 		return nil, err
 	}
-	return swaggerLoader.LoadSwaggerFromDataWithPath(data, location)
+	return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location)
 }
 
 func readUrl(location *url.URL) ([]byte, error) {
@@ -72,6 +82,11 @@ func readUrl(location *url.URL) ([]byte, error) {
 }
 
 func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) {
+	swaggerLoader.reset()
+	return swaggerLoader.loadSwaggerFromFileInternal(path)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) {
 	f := swaggerLoader.LoadSwaggerFromURIFunc
 	if f != nil {
 		return f(swaggerLoader, &url.URL{
@@ -82,12 +97,17 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger,
 	if err != nil {
 		return nil, err
 	}
-	return swaggerLoader.LoadSwaggerFromDataWithPath(data, &url.URL{
+	return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, &url.URL{
 		Path: path,
 	})
 }
 
 func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) {
+	swaggerLoader.reset()
+	return swaggerLoader.loadSwaggerFromDataInternal(data)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*Swagger, error) {
 	swagger := &Swagger{}
 	if err := yaml.Unmarshal(data, swagger); err != nil {
 		return nil, err
@@ -96,6 +116,11 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger,
 }
 
 func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, path *url.URL) (*Swagger, error) {
+	swaggerLoader.reset()
+	return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, path)
+}
+
+func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) {
 	swagger := &Swagger{}
 	if err := yaml.Unmarshal(data, swagger); err != nil {
 		return nil, err
@@ -105,6 +130,9 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, pat
 
 func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.URL) (err error) {
 	swaggerLoader.visited = make(map[interface{}]struct{})
+	if swaggerLoader.visitedFiles == nil {
+		swaggerLoader.visitedFiles = make(map[string]struct{})
+	}
 
 	// Visit all components
 	components := swagger.Components
@@ -145,31 +173,12 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.UR
 	}
 
 	// Visit all operations
-	for _, pathItem := range swagger.Paths {
+	for entrypoint, pathItem := range swagger.Paths {
 		if pathItem == nil {
 			continue
 		}
-		for _, parameter := range pathItem.Parameters {
-			if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
-				return
-			}
-		}
-		for _, operation := range pathItem.Operations() {
-			for _, parameter := range operation.Parameters {
-				if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
-					return
-				}
-			}
-			if requestBody := operation.RequestBody; requestBody != nil {
-				if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, path); err != nil {
-					return
-				}
-			}
-			for _, response := range operation.Responses {
-				if err = swaggerLoader.resolveResponseRef(swagger, response, path); err != nil {
-					return
-				}
-			}
+		if err = swaggerLoader.resolvePathItemRef(swagger, entrypoint, pathItem, path); err != nil {
+			return
 		}
 	}
 
@@ -205,7 +214,23 @@ func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref strin
 	componentPath *url.URL,
 	err error,
 ) {
-	componentPath = path
+	swagger, ref, componentPath, err = swaggerLoader.resolveRefSwagger(swagger, ref, path)
+	if err != nil {
+		return nil, "", nil, err
+	}
+	if !strings.HasPrefix(ref, prefix) {
+		err := fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
+		return nil, "", nil, err
+	}
+	id = ref[len(prefix):]
+	if strings.IndexByte(id, '/') >= 0 {
+		return nil, "", nil, failedToResolveRefFragmentPart(ref, id)
+	}
+	return &swagger.Components, id, componentPath, nil
+}
+
+func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) {
+	componentPath := path
 	if !strings.HasPrefix(ref, "#") {
 		if !swaggerLoader.IsExternalRefsAllowed {
 			return nil, "", nil, fmt.Errorf("Encountered non-allowed external reference: '%s'", ref)
@@ -222,21 +247,13 @@ func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref strin
 			return nil, "", nil, fmt.Errorf("Error while resolving path: %v", err)
 		}
 
-		if swagger, err = swaggerLoader.LoadSwaggerFromURI(resolvedPath); err != nil {
+		if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil {
 			return nil, "", nil, fmt.Errorf("Error while resolving reference '%s': %v", ref, err)
 		}
 		ref = fmt.Sprintf("#%s", fragment)
 		componentPath = resolvedPath
 	}
-	if !strings.HasPrefix(ref, prefix) {
-		err := fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
-		return nil, "", nil, err
-	}
-	id = ref[len(prefix):]
-	if strings.IndexByte(id, '/') >= 0 {
-		return nil, "", nil, failedToResolveRefFragmentPart(ref, id)
-	}
-	return &swagger.Components, id, componentPath, nil
+	return swagger, ref, componentPath, nil
 }
 
 func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, path *url.URL) error {
@@ -567,3 +584,72 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen
 	}
 	return nil
 }
+
+func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypoint string, pathItem *PathItem, path *url.URL) (err error) {
+	// Prevent infinite recursion
+	visited := swaggerLoader.visitedFiles
+	key := "_"
+	if path != nil {
+		key = path.EscapedPath()
+	}
+	key += entrypoint
+	if _, isVisited := visited[key]; isVisited {
+		return nil
+	}
+	visited[key] = struct{}{}
+
+	ref := pathItem.Ref
+	if ref != "" {
+		swagger, ref, path, err = swaggerLoader.resolveRefSwagger(swagger, ref, path)
+		if err != nil {
+			return
+		}
+
+		prefix := "#/paths/"
+		if !strings.HasPrefix(ref, prefix) {
+			err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
+			return
+		}
+		id := unescapeRefString(ref[len(prefix):])
+
+		definitions := swagger.Paths
+		if definitions == nil {
+			return failedToResolveRefFragmentPart(ref, "paths")
+		}
+		resolved := definitions[id]
+		if resolved == nil {
+			return failedToResolveRefFragmentPart(ref, id)
+		}
+
+		*pathItem = *resolved
+	}
+
+	for _, parameter := range pathItem.Parameters {
+		if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
+			return
+		}
+	}
+	for _, operation := range pathItem.Operations() {
+		for _, parameter := range operation.Parameters {
+			if err = swaggerLoader.resolveParameterRef(swagger, parameter, path); err != nil {
+				return
+			}
+		}
+		if requestBody := operation.RequestBody; requestBody != nil {
+			if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, path); err != nil {
+				return
+			}
+		}
+		for _, response := range operation.Responses {
+			if err = swaggerLoader.resolveResponseRef(swagger, response, path); err != nil {
+				return
+			}
+		}
+	}
+
+	return nil
+}
+
+func unescapeRefString(ref string) string {
+	return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1)
+}


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 1265792..8b9da3e 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -36,14 +36,22 @@ func TestConvOpenAPIV2ToV3(t *testing.T) {
 
 const exampleV2 = `
 {
-  "info": {},
+  "info": {"title":"MyAPI","version":"0.1"},
   "schemes": ["https"],
   "host": "test.example.com",
   "basePath": "/v2",
   "paths": {
     "/example": {
       "delete": {
-        "description": "example delete"
+        "description": "example delete",
+        "responses": {
+          "default": {
+            "description": "default response"
+          },
+          "404": {
+            "description": "404 response"
+          }
+        }
       },
       "get": {
         "operationId": "example-get",
@@ -79,19 +87,24 @@ const exampleV2 = `
         ]
       },
       "head": {
-        "description": "example head"
+        "description": "example head",
+        "responses": {}
       },
       "patch": {
-        "description": "example patch"
+        "description": "example patch",
+        "responses": {}
       },
       "post": {
-        "description": "example post"
+        "description": "example post",
+        "responses": {}
       },
       "put": {
-        "description": "example put"
+        "description": "example put",
+        "responses": {}
       },
       "options": {
-        "description": "example options"
+        "description": "example options",
+        "responses": {}
       }
     }
   },
@@ -110,7 +123,7 @@ const exampleV2 = `
 const exampleV3 = `
 {
   "openapi": "3.0",
-  "info": {},
+  "info": {"title":"MyAPI","version":"0.1"},
   "components": {},
   "servers": [
     {
@@ -120,7 +133,15 @@ const exampleV3 = `
   "paths": {
     "/example": {
       "delete": {
-        "description": "example delete"
+        "description": "example delete",
+        "responses": {
+          "default": {
+            "description": "default response"
+          },
+          "404": {
+            "description": "404 response"
+          }
+        }
       },
       "get": {
         "operationId": "example-get",
@@ -158,19 +179,24 @@ const exampleV3 = `
         ]
       },
       "head": {
-        "description": "example head"
+        "description": "example head",
+        "responses": {}
       },
       "options": {
-        "description": "example options"
+        "description": "example options",
+        "responses": {}
       },
       "patch": {
-        "description": "example patch"
+        "description": "example patch",
+        "responses": {}
       },
       "post": {
-        "description": "example post"
+        "description": "example post",
+        "responses": {}
       },
       "put": {
-        "description": "example put"
+        "description": "example put",
+        "responses": {}
       }
     }
   },
diff --git a/openapi3/operation_test.go b/openapi3/operation_test.go
index 8a87723..a3e2e13 100644
--- a/openapi3/operation_test.go
+++ b/openapi3/operation_test.go
@@ -1,6 +1,8 @@
 package openapi3
 
 import (
+	"context"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -30,3 +32,42 @@ func TestAddResponse(t *testing.T) {
 	require.NotNil(t, "status 200", operation.Responses.Get(200).Value)
 	require.NotNil(t, "status 400", operation.Responses.Get(400).Value)
 }
+
+func operationWithoutResponses() *Operation {
+	initOperation()
+	return operation
+}
+
+func operationWithResponses() *Operation {
+	initOperation()
+	operation.AddResponse(200, NewResponse())
+	return operation
+}
+
+func TestOperationValidation(t *testing.T) {
+	tests := []struct {
+		name          string
+		input         *Operation
+		expectedError error
+	}{
+		{
+			"when no Responses object is provided",
+			operationWithoutResponses(),
+			fmt.Errorf("Variable 'Responses' must be a JSON object"),
+		},
+		{
+			"when a Responses object is provided",
+			operationWithResponses(),
+			nil,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			c := context.Background()
+			validationErr := test.input.Validate(c)
+
+			require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match")
+		})
+	}
+}
diff --git a/openapi3/server_test.go b/openapi3/server_test.go
index e9a348e..98fbc7e 100644
--- a/openapi3/server_test.go
+++ b/openapi3/server_test.go
@@ -1,6 +1,8 @@
 package openapi3_test
 
 import (
+	"context"
+	"fmt"
 	"testing"
 
 	"github.com/getkin/kin-openapi/openapi3"
@@ -44,6 +46,44 @@ func TestServerParamValuesNoPath(t *testing.T) {
 	}
 }
 
+func validServer() *openapi3.Server {
+	return &openapi3.Server{
+		URL: "http://my.cool.website",
+	}
+}
+
+func invalidServer() *openapi3.Server {
+	return &openapi3.Server{}
+}
+
+func TestServerValidation(t *testing.T) {
+	tests := []struct {
+		name          string
+		input         *openapi3.Server
+		expectedError error
+	}{
+		{
+			"when no URL is provided",
+			invalidServer(),
+			fmt.Errorf("Variable 'URL' must be a non-empty JSON string"),
+		},
+		{
+			"when a URL is provided",
+			validServer(),
+			nil,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			c := context.Background()
+			validationErr := test.input.Validate(c)
+
+			require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match")
+		})
+	}
+}
+
 func testServerParamValues(t *testing.T, server *openapi3.Server, input string, expected *serverMatch) func(*testing.T) {
 	return func(t *testing.T) {
 		args, remaining, ok := server.MatchRawURL(input)
diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go
index 1e1bcae..c238742 100644
--- a/openapi3/swagger_loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -79,7 +79,7 @@ func ExampleSwaggerLoader() {
 }
 
 func TestResolveSchemaRef(t *testing.T) {
-	source := []byte(`{"info":{"description":"An API"},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`)
+	source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`)
 	loader := openapi3.NewSwaggerLoader()
 	doc, err := loader.LoadSwaggerFromData(source)
 	require.NoError(t, err)
@@ -92,12 +92,12 @@ func TestResolveSchemaRef(t *testing.T) {
 }
 
 func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) {
-	source := []byte(`{"info":{"description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`)
+	source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`)
 	loader := openapi3.NewSwaggerLoader()
 	doc, err := loader.LoadSwaggerFromData(source)
 	require.NoError(t, err)
 	err = doc.Validate(loader.Context)
-	require.EqualError(t, err, "Found unresolved ref: ''")
+	require.EqualError(t, err, "Error when validating Paths: Found unresolved ref: ''")
 }
 
 func TestResolveResponseExampleRef(t *testing.T) {
@@ -168,10 +168,10 @@ func TestResolveSchemaExternalRef(t *testing.T) {
 	rootLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "spec.json"}
 	externalLocation := &url.URL{Scheme: "http", Host: "example.com", Path: "external.json"}
 	rootSpec := []byte(fmt.Sprintf(
-		`{"info":{"description":"An API"},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`,
+		`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Root":{"allOf":[{"$ref":"%s#/components/schemas/External"}]}}}}`,
 		externalLocation.String(),
 	))
-	externalSpec := []byte(`{"info":{"description":"External Spec"},"components":{"schemas":{"External":{"type":"string"}}}}`)
+	externalSpec := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"External Spec"},"paths":{},"components":{"schemas":{"External":{"type":"string"}}}}`)
 	multipleSourceLoader := &multipleSourceSwaggerLoaderExample{
 		Sources: []*sourceExample{
 			{
@@ -423,3 +423,13 @@ func TestLoadYamlFileWithExternalSchemaRef(t *testing.T) {
 
 	require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type)
 }
+
+func TestLoadYamlFileWithExternalPathRef(t *testing.T) {
+	loader := openapi3.NewSwaggerLoader()
+	loader.IsExternalRefsAllowed = true
+	swagger, err := loader.LoadSwaggerFromFile("testdata/pathref.openapi.yml")
+	require.NoError(t, err)
+
+	require.NotNil(t, swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+	require.Equal(t, "string", swagger.Paths["/test"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type)
+}
diff --git a/openapi3/swagger_test.go b/openapi3/swagger_test.go
index a57a124..9ee7f54 100644
--- a/openapi3/swagger_test.go
+++ b/openapi3/swagger_test.go
@@ -1,7 +1,9 @@
 package openapi3_test
 
 import (
+	"context"
 	"encoding/json"
+	"fmt"
 	"testing"
 
 	"github.com/getkin/kin-openapi/openapi3"
@@ -96,7 +98,9 @@ func eqYAML(t *testing.T, expected, actual []byte) {
 
 var specYAML = []byte(`
 openapi: '3.0'
-info: {}
+info:
+  title: MyAPI
+  version: '0.1'
 paths:
   "/hello":
     parameters:
@@ -149,7 +153,10 @@ components:
 var specJSON = []byte(`
 {
   "openapi": "3.0",
-  "info": {},
+  "info": {
+    "title": "MyAPI",
+    "version": "0.1"
+  },
   "paths": {
     "/hello": {
       "parameters": [
@@ -252,6 +259,10 @@ func spec() *openapi3.Swagger {
 	example := map[string]string{"name": "Some example"}
 	return &openapi3.Swagger{
 		OpenAPI: "3.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Paths: openapi3.Paths{
 			"/hello": &openapi3.PathItem{
 				Post: &openapi3.Operation{
@@ -333,3 +344,166 @@ func spec() *openapi3.Swagger {
 		},
 	}
 }
+
+// TestValidation tests validation of properties in the root of the OpenAPI
+// file.
+func TestValidation(t *testing.T) {
+	tests := []struct {
+		name          string
+		input         []byte
+		expectedError error
+	}{
+		{
+			"when no OpenAPI property is supplied",
+			[]byte(`
+info:
+  title: "Hello World REST APIs"
+  version: "1.0"
+paths:
+  "/api/v2/greetings.json":
+    get:
+      operationId: listGreetings
+      responses:
+        200:
+          description: "List different greetings"
+  "/api/v2/greetings/{id}.json":
+    parameters:
+      - name: id
+        in: path
+        required: true
+        schema:
+          type: string
+          example: "greeting"
+    get:
+      operationId: showGreeting
+      responses:
+        200:
+          description: "Get a single greeting object"
+`),
+			fmt.Errorf("Variable 'openapi' must be a non-empty JSON string"),
+		},
+		{
+			"when an empty OpenAPI property is supplied",
+			[]byte(`
+openapi: ''
+info:
+  title: "Hello World REST APIs"
+  version: "1.0"
+paths:
+  "/api/v2/greetings.json":
+    get:
+      operationId: listGreetings
+      responses:
+        200:
+          description: "List different greetings"
+  "/api/v2/greetings/{id}.json":
+    parameters:
+      - name: id
+        in: path
+        required: true
+        schema:
+          type: string
+          example: "greeting"
+    get:
+      operationId: showGreeting
+      responses:
+        200:
+          description: "Get a single greeting object"
+`),
+			fmt.Errorf("Variable 'openapi' must be a non-empty JSON string"),
+		},
+		{
+			"when the Info property is not supplied",
+			[]byte(`
+openapi: '1.0'
+paths:
+  "/api/v2/greetings.json":
+    get:
+      operationId: listGreetings
+      responses:
+        200:
+          description: "List different greetings"
+  "/api/v2/greetings/{id}.json":
+    parameters:
+      - name: id
+        in: path
+        required: true
+        schema:
+          type: string
+          example: "greeting"
+    get:
+      operationId: showGreeting
+      responses:
+        200:
+          description: "Get a single greeting object"
+`),
+			fmt.Errorf("Variable 'info' must be a JSON object"),
+		},
+		{
+			"when the Paths property is not supplied",
+			[]byte(`
+openapi: '1.0'
+info:
+  title: "Hello World REST APIs"
+  version: "1.0"
+`),
+			fmt.Errorf("Variable 'paths' must be a JSON object"),
+		},
+		{
+			"when a valid spec is supplied",
+			[]byte(`
+openapi: 3.0.2
+info:
+  title: "Hello World REST APIs"
+  version: "1.0"
+paths:
+  "/api/v2/greetings.json":
+    get:
+      operationId: listGreetings
+      responses:
+        200:
+          description: "List different greetings"
+  "/api/v2/greetings/{id}.json":
+    parameters:
+      - name: id
+        in: path
+        required: true
+        schema:
+          type: string
+          example: "greeting"
+    get:
+      operationId: showGreeting
+      responses:
+        200:
+          description: "Get a single greeting object"
+components:
+  schemas:
+    GreetingObject:
+      properties:
+        id:
+          type: string
+        type:
+          type: string
+          default: "greeting"
+        attributes:
+          properties:
+            description:
+              type: string
+`),
+			nil,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			doc := &openapi3.Swagger{}
+			err := yaml.Unmarshal(test.input, &doc)
+			require.NoError(t, err)
+
+			c := context.Background()
+			validationErr := doc.Validate(c)
+
+			require.Equal(t, test.expectedError, validationErr, "expected errors (or lack of) to match")
+		})
+	}
+}
diff --git a/openapi3/testdata/circularref.openapi.yml b/openapi3/testdata/circularref.openapi.yml
new file mode 100644
index 0000000..9ee2310
--- /dev/null
+++ b/openapi3/testdata/circularref.openapi.yml
@@ -0,0 +1,19 @@
+---
+openapi: 3.0.0
+info:
+  title: 'OAI Specification in YAML'
+  version: 0.0.1
+paths:
+  /test:
+    get:
+      responses:
+        "200":
+          $ref: '#/components/responses/GetTestOK'
+components:
+  responses:
+    GetTestOK:
+      description: OK
+      content:
+        application/json:
+          schema:
+            $ref: 'pathref.openapi.yml#/components/schemas/TestSchema'
diff --git a/openapi3/testdata/pathref.openapi.yml b/openapi3/testdata/pathref.openapi.yml
new file mode 100644
index 0000000..f09e085
--- /dev/null
+++ b/openapi3/testdata/pathref.openapi.yml
@@ -0,0 +1,12 @@
+---
+openapi: 3.0.0
+info:
+  title: 'OAI Specification in YAML'
+  version: 0.0.1
+paths:
+  /test:
+    $ref: 'circularref.openapi.yml#/paths/~1test'
+components:
+  schemas:
+    TestSchema:
+      type: string
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index bd761b4..d9f6d99 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -844,8 +844,12 @@ func TestDecodeParameter(t *testing.T) {
 						path = "/{" + path + "}"
 					}
 
-					spec := &openapi3.Swagger{}
-					op := &openapi3.Operation{OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}}
+					info := &openapi3.Info{
+						Title:   "MyAPI",
+						Version: "0.1",
+					}
+					spec := &openapi3.Swagger{OpenAPI: "3.0.0", Info: info}
+					op := &openapi3.Operation{OperationID: "test", Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, Responses: make(openapi3.Responses)}
 					spec.AddOperation("/test"+path, http.MethodGet, op)
 					router := NewRouter()
 					require.NoError(t, router.AddSwagger(spec), "failed to create a router")
diff --git a/openapi3filter/router_test.go b/openapi3filter/router_test.go
index 2a7ae3e..0582147 100644
--- a/openapi3filter/router_test.go
+++ b/openapi3filter/router_test.go
@@ -11,17 +11,22 @@ import (
 
 func TestRouter(t *testing.T) {
 	// Build swagger
-	helloCONNECT := &openapi3.Operation{}
-	helloDELETE := &openapi3.Operation{}
-	helloGET := &openapi3.Operation{}
-	helloHEAD := &openapi3.Operation{}
-	helloOPTIONS := &openapi3.Operation{}
-	helloPATCH := &openapi3.Operation{}
-	helloPOST := &openapi3.Operation{}
-	helloPUT := &openapi3.Operation{}
-	helloTRACE := &openapi3.Operation{}
-	paramsGET := &openapi3.Operation{}
+	helloCONNECT := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloDELETE := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloHEAD := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloOPTIONS := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloPATCH := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloPOST := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloPUT := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	helloTRACE := &openapi3.Operation{Responses: make(openapi3.Responses)}
+	paramsGET := &openapi3.Operation{Responses: make(openapi3.Responses)}
 	swagger := &openapi3.Swagger{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Paths: openapi3.Paths{
 			"/hello": &openapi3.PathItem{
 				Connect: helloCONNECT,
diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go
index 3078368..0481f05 100644
--- a/openapi3filter/validation_test.go
+++ b/openapi3filter/validation_test.go
@@ -46,6 +46,11 @@ func TestFilter(t *testing.T) {
 
 	// Declare router
 	swagger := &openapi3.Swagger{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Servers: openapi3.Servers{
 			{
 				URL: "http://example.com/api/",
@@ -71,8 +76,8 @@ func TestFilter(t *testing.T) {
 						},
 						{
 							Value: &openapi3.Parameter{
-								In:   "query",
-								Name: "contentArg",
+								In:      "query",
+								Name:    "contentArg",
 								Content: openapi3.NewContentWithJSONSchema(complexArgSchema),
 							},
 						},
@@ -86,6 +91,7 @@ func TestFilter(t *testing.T) {
 							},
 						},
 					},
+					Responses: make(openapi3.Responses),
 				},
 			},
 		},
@@ -103,9 +109,9 @@ func TestFilter(t *testing.T) {
 
 		// Validate request
 		requestValidationInput := &openapi3filter.RequestValidationInput{
-			Request:    httpReq,
-			PathParams: pathParams,
-			Route:      route,
+			Request:      httpReq,
+			PathParams:   pathParams,
+			Route:        route,
 			ParamDecoder: decoder,
 		}
 		if err := openapi3filter.ValidateRequest(context.TODO(), requestValidationInput); err != nil {
@@ -407,6 +413,11 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 
 	// Create the swagger
 	swagger := &openapi3.Swagger{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Paths: map[string]*openapi3.PathItem{},
 		Security: openapi3.SecurityRequirements{
 			{
@@ -442,7 +453,8 @@ func TestOperationOrSwaggerSecurity(t *testing.T) {
 		}
 		swagger.Paths[tc.name] = &openapi3.PathItem{
 			Get: &openapi3.Operation{
-				Security: securityRequirements,
+				Security:  securityRequirements,
+				Responses: make(openapi3.Responses),
 			},
 		}
 	}
@@ -540,6 +552,11 @@ func TestAnySecurityRequirementMet(t *testing.T) {
 
 	// Create the swagger
 	swagger := openapi3.Swagger{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Paths: map[string]*openapi3.PathItem{},
 		Components: openapi3.Components{
 			SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{},
@@ -569,7 +586,8 @@ func TestAnySecurityRequirementMet(t *testing.T) {
 		// Create the path with the security requirements
 		swagger.Paths[tc.name] = &openapi3.PathItem{
 			Get: &openapi3.Operation{
-				Security: &securityRequirements,
+				Security:  &securityRequirements,
+				Responses: make(openapi3.Responses),
 			},
 		}
 	}
@@ -631,6 +649,11 @@ func TestAllSchemesMet(t *testing.T) {
 
 	// Create the swagger
 	swagger := openapi3.Swagger{
+		OpenAPI: "3.0.0",
+		Info: &openapi3.Info{
+			Title:   "MyAPI",
+			Version: "0.1",
+		},
 		Paths: map[string]*openapi3.PathItem{},
 		Components: openapi3.Components{
 			SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{},
@@ -664,6 +687,7 @@ func TestAllSchemesMet(t *testing.T) {
 				Security: &openapi3.SecurityRequirements{
 					securityRequirement,
 				},
+				Responses: make(openapi3.Responses),
 			},
 		}
 	}