Validating OpenAPI contacts, requests and responses

Validate http requests, responses, schemas and OpenAPI 3+ contracts

Get libopenapi-validator

The first step to validation, is to get the libopenapi-validator module added to the project, validation does not come with libopenapi as a core module.

libopenapi-validator depends on github.com/santhosh-tekuri/jsonschema as the validation engine for schemas. This dependency is only required for validation and not for the core libopenapi library to operate.

To avoid having folks import dependencies that are only used for a certain capability, we have kept validation as an opt-in feature, if the host application requires it.

go get github.com/pb33f/libopenapi-validator

The structure of the validator

libopenapi-validator is composed of five core packages, each providing a type of validation:

  • parameters
  • paths
  • requests
  • responses
  • schema_validation **

**Schema validation is handled slightly differently to the other four, but follow the same overall designs.

Each package can be used independently, as each one has its own XXValidator interface.

Each package validator can be created from an existing libopenapi.Document instance, using the package level NewXXValidator function.

For example, to create a new ParameterValidator the code would be:

import (
    "github.com/pb33f/libopenapi"
    "github.com/pb33f/libopenapi-validator/parameters"
 )
 
func createValidator() {
    
  // 1. Load an OpenAPI Spec into bytes
  petstore, err := os.ReadFile("test_specs/petstorev3.json")

  if err != nil {
      panic(err)
  }

  // 2. Create a new OpenAPI document using libopenapi
  document, docErrs := libopenapi.NewDocument(petstore)

  if docErrs != nil {
    panic(docErrs)
  }

  // 3. Create a new ParameterValidator
  parameterValidator, validatorErrs := parameters.NewParameterValidator(document)
    
  // ... do something useful
}

Validation errors

Every validation method or function tha validates something in libopenapi-validator returns a slice of errors.ValidationError pointers.

ValidationError is a struct, and it has the following properties.

// ValidationError is a struct that contains all the information about a validation error.
type ValidationError struct {

  // Message is a human-readable message describing the error.
  Message string

  // Reason is a human-readable message describing the reason for the error.
  Reason string

  // ValidationType is a string that describes the type of validation that failed.
  ValidationType string

  // ValidationSubType is a string that describes the subtype of validation that failed.
  ValidationSubType string

  // SpecLine is the line number in the spec where the error occurred.
  SpecLine int

  // SpecCol is the column number in the spec where the error occurred.
  SpecCol int

  // HowToFix is a human-readable message describing how to fix the error.
  HowToFix string

  // SchemaValidationErrors is a slice of SchemaValidationFailure objects that describe the validation errors
  // This is only populated whe the validation type is against a schema.
  SchemaValidationErrors []*SchemaValidationFailure

  // Context is the object that the validation error occurred on. This is usually a pointer to a schema
  // or a parameter object.
  Context interface{}
}

Schema errors

If the error is schema error, then the SchemaValidationErrors property on ValidationError will contain a slice of errors.SchemaValidationFailure pointers.

The SchemaValidationFailure struct looks like this:

type SchemaValidationFailure struct {
  // Reason is a human-readable message describing the reason for the error. 
  Reason string

  // Location is the XPath-like location of the validation failure
  Location string
  
  // DeepLocation is the path to the validation failure as exposed by the jsonschema library.
  DeepLocation string `json:"deepLocation,omitempty" yaml:"deepLocation,omitempty"`

  // AbsoluteLocation is the absolute path to the validation failure as exposed by the jsonschema library.
  AbsoluteLocation string `json:"absoluteLocation,omitempty" yaml:"absoluteLocation,omitempty"`

  // Line is the line number where the violation occurred. This may a local line number
  // if the validation is a schema (only schemas are validated locally, so the line number will be relative to
  // the Context object held by the ValidationError object).
  Line int

  // Column is the column number where the violation occurred. This may a local column number
  // if the validation is a schema (only schemas are validated locally, so the column number will be relative to
  // the Context object held by the ValidationError object).
  Column int

  // The original error object, which is a jsonschema.ValidationError object.
  OriginalError *jsonschema.ValidationError
}

The Line and Column are relative to the schema that evaluated, not the overall OpenAPI document.

All schemas are resolved into ‘inline’ renders, in order to be able to validate them. The schema is assembled into a fully resolved JSON Schema, meaning all $ref values will be compiled into inline data.

Be careful of infinite circular references.

High-level validation

The validator provides an overall validator, for simple access to three core elements of validation that will suit most requirements:

  • Request Validation
  • Request & Response Validation
  • Document Validation

The high-level validator can be created using NewValidator() and passing in a libopenapi.Document instance.

import (
  "github.com/pb33f/libopenapi"
  validator "github.com/pb33f/libopenapi-validator"
)

func doSomething() {
    
  // 1. Load an OpenAPI Spec into bytes
  petstore, err := os.ReadFile("test_specs/petstorev3.json")

  if err != nil {
      panic(err)
  }

  // 2. Create a new OpenAPI document using libopenapi
  document, docErrs := libopenapi.NewDocument(petstore)

  if docErrs != nil {
    panic(docErrs)
  }

  // 3. Create a new Validator
  highLevelValidator, validatorErrs := validator.NewValidator(document)
    
  // ... do something useful
    
}

The interface for the high level validator has the following signature:

type Validator interface {
  ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError)
  ValidateHttpRequestResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError)
  ValidateDocument() (bool, []*errors.ValidationError)
  
  // To access individual validators
  GetParameterValidator() parameters.ParameterValidator
  GetRequestBodyValidator() requests.RequestBodyValidator
  GetResponseBodyValidator() responses.ResponseBodyValidator
}

Validating http.Request

If the application is handling HTTP traffic and validation for incoming requests against an OpenAPI specification is required, then validating a request is straight forward.

import (
  "github.com/pb33f/libopenapi"
  validator "github.com/pb33f/libopenapi-validator"
  "net/http"
)

func doSomething() {
    
  // ... create OpenAPI Document

  // Create a new Validator
  highLevelValidator, validatorErrs := validator.NewValidator(document)
  if len(validatorErrs) > 0 {
      panic("document is bad")
  }    
  
  var request *http.Request
    
  // ... the application populates the request
  
  // Validate the request  
  requestValid, validationErrors := highLevelValidator.ValidateHttpRequest(request)
    
  if !requestValid {
      for i := range validationErrors {
         fmt.Println(validationErrors[i].Message) // or something.
      }
  }  
}

Validating http.Request and http.Response

If the application is handling HTTP traffic and validation for requests and responses against an OpenAPI specification is required, then validating a request/response is also straight forward.

import (
  "github.com/pb33f/libopenapi"
  validator "github.com/pb33f/libopenapi-validator"
  "net/http"
)

func doSomething() {
    
  // ... create OpenAPI Document

  // Create a new Validator
  highLevelValidator, validatorErrs := validator.NewValidator(document)
  if len(validatorErrs) > 0 {
      panic("document is bad")
  }    
  
  var request *http.Request
  var response *http.Response
    
  // ... the application populates the request
  
  // Validate the request and the response
  requestValid, validationErrors := highLevelValidator.ValidateHttpRequestResponse(request, response)
    
  if !requestValid {
      for i := range validationErrors {
         fmt.Println(validationErrors[i].Message) // or something.
      }
  }  
}

Validating just http.Response

Don’t care about the request? Just want to validate the response? No problem, however you will still need the request, because it’s used as the lookup to find the response schema.

In the responses package, there is a ResponseBodyValidator interface that can be used to validate a response body only.

import (
    "github.com/pb33f/libopenapi"
    "github.com/pb33f/libopenapi-validator/responses"
 )
 
func createValidator() {
    
  // ... create an OpenAPI Document

  // 3. Create a new ResponseBodyValidator
  rbValidator, _ := parameters.NewResponseBodyValidator(document)
    
  // The *http.Request pointer is still required, however it is not validated by this validator.
  // it's used to lookup the path/operation/schemas  
  validBody, errors := rvValidator.ValidateResponseBody(request, response)
  
  // check the errors...
  
}

Validating HTTP Parameters

When validating HTTP parameters, the ParameterValidator interface can be used to validate the parameters specifically. It may be useful if the need to validate the parameters in isolation is required.

The ParameterValidator interface has the following signature:

import (
    "github.com/pb33f/libopenapi-validator/errors"
    "github.com/pb33f/libopenapi/datamodel/high/v3"
    "net/http"
 )

type ParameterValidator interface {

  // SetPathItem will set the pathItem for the ParameterValidator, all validations will be performed against this pathItem
  // otherwise if not set, each validation will perform a lookup for the pathItem based on the *http.Request
  SetPathItem(path *v3.PathItem, pathValue string)
    
  // ValidateQueryParams accepts an *http.Request and validates the query parameters against the OpenAPI specification.
  // The method will locate the correct path, and operation, based on the verb. The parameters for the operation
  // will be matched and validated against what has been supplied in the http.Request query string.
  ValidateQueryParams(request *http.Request) (bool, []*errors.ValidationError)

  // ValidateHeaderParams validates the header parameters contained within *http.Request. It returns a boolean
  // stating true if validation passed (false for failed), and a slice of errors if validation failed.
  ValidateHeaderParams(request *http.Request) (bool, []*errors.ValidationError)

  // ValidateCookieParams validates the cookie parameters contained within *http.Request.
  // It returns a boolean stating true if validation passed (false for failed), and a slice of errors if validation failed.
  ValidateCookieParams(request *http.Request) (bool, []*errors.ValidationError)

  // ValidatePathParams validates the path parameters contained within *http.Request. It returns a boolean stating true
  // if validation passed (false for failed), and a slice of errors if validation failed.
  ValidatePathParams(request *http.Request) (bool, []*errors.ValidationError)
}

Validating an OpenAPI document

Want to validate the raw OpenAPI document? No problem, the ValidateDocument() method on the high-level validator will do that for you.



  // 3. Create a new validator
  docValidator, validatorErrs := NewValidator(document)

  if validatorErrs != nil {
      panic(validatorErrs)
  }

  // 4. Validate!
  valid, validationErrs := docValidator.ValidateDocument()

  if !valid {
      for i, e := range validationErrs {
          // 5. Handle the error
          fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message)
          fmt.Printf("Fix: %s\n\n", e.HowToFix)
      } 
  } 
}

Which would output something like this:

Type: schema, Failure: Document does not pass validation Fix: Ensure that the object being submitted, matches the schema correctly

The schema violations will be populated on the SchemaValidationErrors property of the ValidationError struct pointer.


Validating Schemas

Once a valid libopenapi.Document exists, any high-level base.Schema can be used to validate a blob of JSON or YAML, or even an unmarshalled JSON/YAML object.

The schema_validator package contains a SchemaValidator interface with the following signature:

type SchemaValidator interface {

  // ValidateSchemaString accepts a schema object to validate against, and a JSON/YAML blob that is defined as a string.
  ValidateSchemaString(schema *base.Schema, payload string) (bool, []*errors.ValidationError)

  // ValidateSchemaObject accepts a schema object to validate against, and an object, created from unmarshalled JSON/YAML.
  // This is a pre-decoded object that will skip the need to unmarshal a string of JSON/YAML.
  ValidateSchemaObject(schema *base.Schema, payload interface{}) (bool, []*errors.ValidationError)

  // ValidateSchemaBytes accepts a schema object to validate against, and a byte slice containing a schema to
  // validate against.
  ValidateSchemaBytes(schema *base.Schema, payload []byte) (bool, []*errors.ValidationError)
}

To create a new SchemaValidator instance, use the NewSchemaValidator() function.