Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ type RegexCache interface {
//
// Generally fluent With... style functions are used to establish the desired behavior.
type ValidationOptions struct {
RegexEngine jsonschema.RegexpEngine
RegexCache RegexCache // Enable compiled regex caching
FormatAssertions bool
ContentAssertions bool
SecurityValidation bool
OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation
AllowScalarCoercion bool // Enable string->boolean/number coercion
Formats map[string]func(v any) error
SchemaCache cache.SchemaCache // Optional cache for compiled schemas
Logger *slog.Logger // Logger for debug/error output (nil = silent)
RegexEngine jsonschema.RegexpEngine
RegexCache RegexCache // Enable compiled regex caching
FormatAssertions bool
ContentAssertions bool
SecurityValidation bool
OpenAPIMode bool // Enable OpenAPI-specific vocabulary validation
AllowScalarCoercion bool // Enable string->boolean/number coercion
Formats map[string]func(v any) error
SchemaCache cache.SchemaCache // Optional cache for compiled schemas
Logger *slog.Logger // Logger for debug/error output (nil = silent)
AllowXMLBodyValidation bool // Allows to convert XML to JSON when validating a request/response body.

// strict mode options - detect undeclared properties even when additionalProperties: true
StrictMode bool // Enable strict property validation
Expand Down Expand Up @@ -75,6 +76,7 @@ func WithExistingOpts(options *ValidationOptions) Option {
o.Formats = options.Formats
o.SchemaCache = options.SchemaCache
o.Logger = options.Logger
o.AllowXMLBodyValidation = options.AllowXMLBodyValidation
o.StrictMode = options.StrictMode
o.StrictIgnorePaths = options.StrictIgnorePaths
o.StrictIgnoredHeaders = options.StrictIgnoredHeaders
Expand Down Expand Up @@ -161,6 +163,14 @@ func WithScalarCoercion() Option {
}
}

// WithXmlBodyValidation enables converting an XML body to a JSON when validating the schema from a request and response body
// The default option is set to false
func WithXmlBodyValidation() Option {
return func(o *ValidationOptions) {
o.AllowXMLBodyValidation = true
}
}

// WithSchemaCache sets a custom cache implementation or disables caching if nil.
// Pass nil to disable schema caching and skip cache warming during validator initialization.
// The default cache is a thread-safe sync.Map wrapper.
Expand Down
39 changes: 24 additions & 15 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ func TestNewValidationOptions_Defaults(t *testing.T) {
assert.False(t, opts.FormatAssertions)
assert.False(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
}
Expand All @@ -32,8 +33,9 @@ func TestNewValidationOptions_WithNilOption(t *testing.T) {
assert.False(t, opts.FormatAssertions)
assert.False(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
}
Expand All @@ -44,8 +46,9 @@ func TestWithFormatAssertions(t *testing.T) {
assert.True(t, opts.FormatAssertions)
assert.False(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
}
Expand All @@ -56,8 +59,9 @@ func TestWithContentAssertions(t *testing.T) {
assert.False(t, opts.FormatAssertions)
assert.True(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
}
Expand Down Expand Up @@ -93,18 +97,20 @@ func TestWithExistingOpts(t *testing.T) {
// Create original options with all settings enabled
var testEngine jsonschema.RegexpEngine = nil
original := &ValidationOptions{
RegexEngine: testEngine,
RegexCache: &sync.Map{},
FormatAssertions: true,
ContentAssertions: true,
SecurityValidation: false,
RegexEngine: testEngine,
RegexCache: &sync.Map{},
FormatAssertions: true,
AllowXMLBodyValidation: true,
ContentAssertions: true,
SecurityValidation: false,
}

// Create new options using existing options
opts := NewValidationOptions(WithExistingOpts(original))

assert.Nil(t, opts.RegexEngine) // Both should be nil
assert.NotNil(t, opts.RegexCache)
assert.Equal(t, original.AllowXMLBodyValidation, opts.AllowXMLBodyValidation)
assert.Equal(t, original.FormatAssertions, opts.FormatAssertions)
assert.Equal(t, original.ContentAssertions, opts.ContentAssertions)
assert.Equal(t, original.SecurityValidation, opts.SecurityValidation)
Expand All @@ -119,8 +125,9 @@ func TestWithExistingOpts_NilSource(t *testing.T) {
assert.False(t, opts.FormatAssertions)
assert.False(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
}
Expand All @@ -129,11 +136,13 @@ func TestMultipleOptions(t *testing.T) {
opts := NewValidationOptions(
WithFormatAssertions(),
WithContentAssertions(),
WithXmlBodyValidation(),
)

assert.True(t, opts.FormatAssertions)
assert.True(t, opts.ContentAssertions)
assert.True(t, opts.SecurityValidation)
assert.True(t, opts.AllowXMLBodyValidation)
assert.True(t, opts.OpenAPIMode) // Default is true
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.Nil(t, opts.RegexEngine)
Expand Down
27 changes: 16 additions & 11 deletions errors/parameters_howtofix.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ const (
HowToFixParamInvalidBoolean string = "Convert the value '%s' into a true/false value"
HowToFixParamInvalidEnum string = "Instead of '%s', use one of the allowed values: '%s'"
HowToFixParamInvalidFormEncode string = "Use a form style encoding for parameter values, for example: '%s'"
HowToFixInvalidXml string = "Ensure xml is well-formed and matches schema structure"
HowToFixXmlPrefix string = "Make sure to prepend the correct prefix '%s' to the declared fields"
HowToFixXmlNamespace string = "Make sure to declare the 'xmlns:%s' with the correct namespace URI"
HowToFixInvalidSchema string = "Ensure that the object being submitted, matches the schema correctly"
HowToFixParamInvalidSpaceDelimitedObjectExplode string = "When using 'explode' with space delimited parameters, " +
"they should be separated by spaces. For example: '%s'"
HowToFixParamInvalidPipeDelimitedObjectExplode string = "When using 'explode' with pipe delimited parameters, " +
"they should be separated by pipes '|'. For example: '%s'"
HowToFixParamInvalidDeepObjectMultipleValues string = "There can only be a single value per property name, " +
"deepObject parameters should contain the property key in square brackets next to the parameter name. For example: '%s'"
HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax"
HowToFixDecodingError = "The object can't be decoded, so make sure it's being encoded correctly according to the spec."
HowToFixInvalidContentType = "The content type is invalid, Use one of the %d supported types for this operation: %s"
HowToFixInvalidResponseCode = "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification"
HowToFixInvalidEncoding = "Ensure the correct encoding has been used on the object"
HowToFixMissingValue = "Ensure the value has been set"
HowToFixPath = "Check the path is correct, and check that the correct HTTP method has been used (e.g. GET, POST, PUT, DELETE)"
HowToFixPathMethod = "Add the missing operation to the contract for the path"
HowToFixInvalidMaxItems = "Reduce the number of items in the array to %d or less"
HowToFixInvalidMinItems = "Increase the number of items in the array to %d or more"
HowToFixMissingHeader = "Make sure the service responding sets the required headers with this response code"
HowToFixInvalidJSON string = "The JSON submitted is invalid, please check the syntax"
HowToFixDecodingError string = "The object can't be decoded, so make sure it's being encoded correctly according to the spec."
HowToFixInvalidContentType string = "The content type is invalid, Use one of the %d supported types for this operation: %s"
HowToFixInvalidResponseCode string = "The service is responding with a code that is not defined in the spec, fix the service or add the code to the specification"
HowToFixInvalidEncoding string = "Ensure the correct encoding has been used on the object"
HowToFixMissingValue string = "Ensure the value has been set"
HowToFixPath string = "Check the path is correct, and check that the correct HTTP method has been used (e.g. GET, POST, PUT, DELETE)"
HowToFixPathMethod string = "Add the missing operation to the contract for the path"
HowToFixInvalidMaxItems string = "Reduce the number of items in the array to %d or less"
HowToFixInvalidMinItems string = "Increase the number of items in the array to %d or more"
HowToFixMissingHeader string = "Make sure the service responding sets the required headers with this response code"
HowToFixInvalidRenderedSchema string = "Check the request schema for circular references or invalid structures"
HowToFixInvalidJsonSchema string = "Check the request schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs"
)
105 changes: 105 additions & 0 deletions errors/xml_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package errors

import (
"fmt"

"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi/datamodel/high/base"
)

func MissingPrefix(schema *base.Schema, prefix string) *ValidationError {
line := 1
col := 0
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
line = low.Type.KeyNode.Line
col = low.Type.KeyNode.Column
}

return &ValidationError{
ValidationType: helpers.XmlValidation,
ValidationSubType: helpers.XmlValidationPrefix,
Message: fmt.Sprintf("The prefix '%s' is defined in the schema, however it's missing from the xml", prefix),
Reason: fmt.Sprintf("The prefix '%s' is defined in the schema, however it's missing from the xml content", prefix),
SpecLine: line,
SpecCol: col,
Context: schema,
HowToFix: fmt.Sprintf(HowToFixXmlPrefix, prefix),
}
}

func InvalidPrefix(schema *base.Schema, prefix string) *ValidationError {
line := 1
col := 0
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
line = low.Type.KeyNode.Line
col = low.Type.KeyNode.Column
}

return &ValidationError{
ValidationType: helpers.XmlValidation,
ValidationSubType: helpers.XmlValidationPrefix,
Message: fmt.Sprintf("The prefix '%s' defined in the schema differs from the xml", prefix),
Reason: fmt.Sprintf("The prefix '%s' is defined in the schema, however the xml sent and invalid prefix", prefix),
SpecCol: col,
SpecLine: line,
Context: schema,
HowToFix: fmt.Sprintf(HowToFixXmlPrefix, prefix),
}
}

func MissingNamespace(schema *base.Schema, namespace string) *ValidationError {
line := 1
col := 0
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
line = low.Type.KeyNode.Line
col = low.Type.KeyNode.Column
}

return &ValidationError{
ValidationType: helpers.XmlValidation,
ValidationSubType: helpers.XmlValidationNamespace,
Message: fmt.Sprintf("The namespace '%s' is defined in the schema, however it's missing from the xml", namespace),
Reason: fmt.Sprintf("The namespace '%s' is defined in the schema, however it's missing from the xml content", namespace),
SpecLine: line,
SpecCol: col,
Context: schema,
HowToFix: fmt.Sprintf(HowToFixXmlNamespace, namespace),
}
}

func InvalidNamespace(schema *base.Schema, namespace, expectedNamespace, prefix string) *ValidationError {
line := 1
col := 0
if low := schema.GoLow(); low != nil && low.Type.KeyNode != nil {
line = low.Type.KeyNode.Line
col = low.Type.KeyNode.Column
}

return &ValidationError{
ValidationType: helpers.XmlValidation,
ValidationSubType: helpers.XmlValidationNamespace,
Message: fmt.Sprintf("The namespace from prefix '%s' differs from the xml", prefix),
Reason: fmt.Sprintf("The namespace from prefix '%s' is declared as '%s' in the schema, however in xml is declared as '%s'",
prefix, expectedNamespace, namespace),
SpecLine: line,
SpecCol: col,
Context: schema,
HowToFix: fmt.Sprintf(HowToFixXmlNamespace, namespace),
}
}

func InvalidXmlParsing(reason, referenceObject string) *ValidationError {
return &ValidationError{
ValidationType: helpers.XmlValidation,
ValidationSubType: helpers.Schema,
Message: "xml example is malformed",
Reason: fmt.Sprintf("failed to parse xml: %s", reason),
SchemaValidationErrors: []*SchemaValidationFailure{{
Reason: reason,
Location: "xml parsing",
ReferenceSchema: "",
ReferenceObject: referenceObject,
}},
HowToFix: HowToFixInvalidXml,
}
}
77 changes: 77 additions & 0 deletions errors/xml_errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley
// https://pb33f.io

package errors

import (
"testing"

"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/stretchr/testify/assert"
)

func getTestSchema() *base.Schema {
spec := `openapi: 3.0.0
paths:
/pet:
get:
responses:
'200':
content:
application/xml:
schema:
type: object
properties:
age:
type: integer
xml:
name: Cat`

doc, _ := libopenapi.NewDocument([]byte(spec))
v3Doc, _ := doc.BuildV3Model()

return v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200").
Content.GetOrZero("application/xml").Schema.Schema()
}

func TestMissingPrefixError(t *testing.T) {
schema := getTestSchema()
err := MissingPrefix(schema, "prx")

assert.NotNil(t, *err)
assert.Equal(t, helpers.XmlValidationPrefix, (*err).ValidationSubType)
}

func TestMissingNamespaceError(t *testing.T) {
schema := getTestSchema()
err := MissingNamespace(schema, "http://ex.c")

assert.NotNil(t, *err)
assert.Equal(t, helpers.XmlValidationNamespace, (*err).ValidationSubType)
}

func TestInvalidPrefixError(t *testing.T) {
schema := getTestSchema()
err := InvalidPrefix(schema, "prx")

assert.NotNil(t, *err)
assert.Equal(t, helpers.XmlValidationPrefix, (*err).ValidationSubType)
}

func TestInvalidNamespaceError(t *testing.T) {
schema := getTestSchema()
err := InvalidNamespace(schema, "other", "http://ex.c", "prx")

assert.NotNil(t, *err)
assert.Equal(t, helpers.XmlValidationNamespace, (*err).ValidationSubType)
}

func TestInvalidParsing(t *testing.T) {
err := InvalidXmlParsing("no data sent", "invalid-xml")

assert.NotNil(t, (*err))
assert.Equal(t, (*err).SchemaValidationErrors[0].Location, "xml parsing")
assert.Equal(t, helpers.Schema, (*err).ValidationSubType)
}
3 changes: 3 additions & 0 deletions helpers/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const (
ParameterValidationCookie = "cookie"
RequestValidation = "request"
RequestBodyValidation = "requestBody"
XmlValidation = "xmlValidation"
XmlValidationPrefix = "prefix"
XmlValidationNamespace = "namespace"
Schema = "schema"
ResponseBodyValidation = "response"
RequestBodyContentType = "contentType"
Expand Down
Loading