diff --git a/config/config.go b/config/config.go
index f46acce..fa95c65 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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
@@ -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
@@ -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.
diff --git a/config/config_test.go b/config/config_test.go
index dddd739..40ea92d 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -93,11 +97,12 @@ 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
@@ -105,6 +110,7 @@ func TestWithExistingOpts(t *testing.T) {
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)
@@ -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)
}
@@ -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)
diff --git a/errors/parameters_howtofix.go b/errors/parameters_howtofix.go
index e20a150..bec9bcd 100644
--- a/errors/parameters_howtofix.go
+++ b/errors/parameters_howtofix.go
@@ -12,6 +12,9 @@ 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'"
@@ -19,15 +22,17 @@ const (
"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"
)
diff --git a/errors/xml_errors.go b/errors/xml_errors.go
new file mode 100644
index 0000000..5c81f0c
--- /dev/null
+++ b/errors/xml_errors.go
@@ -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,
+ }
+}
diff --git a/errors/xml_errors_test.go b/errors/xml_errors_test.go
new file mode 100644
index 0000000..5c4552a
--- /dev/null
+++ b/errors/xml_errors_test.go
@@ -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)
+}
diff --git a/helpers/constants.go b/helpers/constants.go
index 1faecff..53fe27a 100644
--- a/helpers/constants.go
+++ b/helpers/constants.go
@@ -11,6 +11,9 @@ const (
ParameterValidationCookie = "cookie"
RequestValidation = "request"
RequestBodyValidation = "requestBody"
+ XmlValidation = "xmlValidation"
+ XmlValidationPrefix = "prefix"
+ XmlValidationNamespace = "namespace"
Schema = "schema"
ResponseBodyValidation = "response"
RequestBodyContentType = "contentType"
diff --git a/requests/validate_body.go b/requests/validate_body.go
index e9aad70..c8ce603 100644
--- a/requests/validate_body.go
+++ b/requests/validate_body.go
@@ -4,7 +4,10 @@
package requests
import (
+ "bytes"
+ "encoding/json"
"fmt"
+ "io"
"net/http"
"strings"
@@ -14,6 +17,7 @@ import (
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/paths"
+ "github.com/pb33f/libopenapi-validator/schema_validation"
)
func (v *requestBodyValidator) ValidateRequestBody(request *http.Request) (bool, []*errors.ValidationError) {
@@ -66,12 +70,6 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req
return false, []*errors.ValidationError{errors.RequestContentTypeNotFound(operation, request, pathValue)}
}
- // we currently only support JSON validation for request bodies
- // this will capture *everything* that contains some form of 'json' in the content type
- if !strings.Contains(strings.ToLower(contentType), helpers.JSONType) {
- return true, nil
- }
-
// Nothing to validate
if mediaType.Schema == nil {
return true, nil
@@ -80,6 +78,32 @@ func (v *requestBodyValidator) ValidateRequestBodyWithPathItem(request *http.Req
// extract schema from media type
schema := mediaType.Schema.Schema()
+ if !strings.Contains(strings.ToLower(contentType), helpers.JSONType) {
+ // we currently only support JSON and XML validation for request bodies
+ // this will capture *everything* that contains some form of 'json' in the content type
+ if !v.options.AllowXMLBodyValidation || !schema_validation.IsXMLContentType(contentType) {
+ return true, nil
+ }
+
+ if request != nil && request.Body != nil {
+ requestBody, _ := io.ReadAll(request.Body)
+ _ = request.Body.Close()
+
+ stringedBody := string(requestBody)
+ jsonBody, prevalidationErrors := schema_validation.TransformXMLToSchemaJSON(stringedBody, schema)
+ if len(prevalidationErrors) > 0 {
+ return false, prevalidationErrors
+ }
+
+ transformedBytes, err := json.Marshal(jsonBody)
+ if err != nil {
+ return false, []*errors.ValidationError{errors.InvalidXmlParsing(err.Error(), stringedBody)}
+ }
+
+ request.Body = io.NopCloser(bytes.NewBuffer(transformedBytes))
+ }
+ }
+
validationSucceeded, validationErrors := ValidateRequestSchema(&ValidateRequestSchemaInput{
Request: request,
Schema: schema,
diff --git a/requests/validate_body_test.go b/requests/validate_body_test.go
index bc96085..7d98fe2 100644
--- a/requests/validate_body_test.go
+++ b/requests/validate_body_test.go
@@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/pb33f/libopenapi-validator/config"
+ "github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/paths"
)
@@ -1576,3 +1577,121 @@ paths:
assert.True(t, valid)
assert.Len(t, errors, 0)
}
+
+func TestValidateBody_XmlRequest(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ requestBody:
+ content:
+ application/xml:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer
+ xml:
+ name: cost`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := "cheeseburger23"
+
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
+ bytes.NewBuffer([]byte(body)))
+ request.Header.Set("Content-Type", "application/xml")
+
+ valid, errors := v.ValidateRequestBody(request)
+
+ assert.True(t, valid)
+ assert.Len(t, errors, 0)
+}
+
+func TestValidateBody_XmlMalformedRequest(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ requestBody:
+ content:
+ application/xml:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer
+ xml:
+ name: cost`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := ""
+
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
+ bytes.NewBuffer([]byte(body)))
+ request.Header.Set("Content-Type", "application/xml")
+
+ valid, errors := v.ValidateRequestBody(request)
+
+ assert.False(t, valid)
+ assert.Len(t, errors, 1)
+
+ err := errors[0]
+ assert.Equal(t, helpers.XmlValidation, err.ValidationType)
+ assert.Contains(t, err.Reason, "failed to parse xml")
+}
+
+func TestValidateBody_XmlRequestTransformations(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ requestBody:
+ content:
+ application/xml:
+ schema:
+ type: object
+ xml:
+ name: Burger
+ required:
+ - name
+ - patties
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer
+ xml:
+ name: cost`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewRequestBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := "cheeseburger23"
+
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger",
+ bytes.NewBuffer([]byte(body)))
+ request.Header.Set("Content-Type", "application/xml")
+
+ valid, errors := v.ValidateRequestBody(request)
+
+ assert.True(t, valid)
+ assert.Len(t, errors, 0)
+}
diff --git a/requests/validate_request.go b/requests/validate_request.go
index f1ce93c..8383200 100644
--- a/requests/validate_request.go
+++ b/requests/validate_request.go
@@ -100,7 +100,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V
SpecLine: 1,
SpecCol: 0,
SchemaValidationErrors: []*errors.SchemaValidationFailure{violation},
- HowToFix: "check the request schema for circular references or invalid structures",
+ HowToFix: errors.HowToFixInvalidRenderedSchema,
Context: referenceSchema,
})
return false, validationErrors
diff --git a/responses/validate_body.go b/responses/validate_body.go
index ae09b30..ca28970 100644
--- a/responses/validate_body.go
+++ b/responses/validate_body.go
@@ -4,7 +4,10 @@
package responses
import (
+ "bytes"
+ "encoding/json"
"fmt"
+ "io"
"net/http"
"strconv"
"strings"
@@ -17,6 +20,7 @@ import (
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/paths"
+ "github.com/pb33f/libopenapi-validator/schema_validation"
)
func (v *responseBodyValidator) ValidateResponseBody(
@@ -131,26 +135,55 @@ func (v *responseBodyValidator) checkResponseSchema(
) []*errors.ValidationError {
var validationErrors []*errors.ValidationError
- // currently, we can only validate JSON based responses, so check for the presence
- // of 'json' in the content type (what ever it may be) so we can perform a schema check on it.
- // anything other than JSON, will be ignored.
- if strings.Contains(strings.ToLower(contentType), helpers.JSONType) {
- // extract schema from media type
- if mediaType.Schema != nil {
- schema := mediaType.Schema.Schema()
-
- // Validate response schema
- valid, vErrs := ValidateResponseSchema(&ValidateResponseSchemaInput{
- Request: request,
- Response: response,
- Schema: schema,
- Version: helpers.VersionToFloat(v.document.Version),
- Options: []config.Option{config.WithExistingOpts(v.options)},
- })
- if !valid {
- validationErrors = append(validationErrors, vErrs...)
+ if mediaType.Schema == nil {
+ return validationErrors
+ }
+
+ // currently, we can only validate JSON and XML based responses, so check for the presence
+ // of 'json' (what ever it may be) and for XML content type so we can perform a schema check on it.
+ // anything other than JSON or XML, will be ignored.
+
+ isXml := schema_validation.IsXMLContentType(contentType)
+
+ if !strings.Contains(strings.ToLower(contentType), helpers.JSONType) && (!v.options.AllowXMLBodyValidation || !isXml) {
+ return validationErrors
+ }
+
+ schema := mediaType.Schema.Schema()
+
+ if isXml {
+ if response != nil && response.Body != http.NoBody {
+ responseBody, _ := io.ReadAll(response.Body)
+ _ = response.Body.Close()
+
+ stringedBody := string(responseBody)
+ jsonBody, prevalidationErrors := schema_validation.TransformXMLToSchemaJSON(stringedBody, schema)
+
+ if len(prevalidationErrors) > 0 {
+ return prevalidationErrors
+ }
+
+ transformedBytes, err := json.Marshal(jsonBody)
+ if err != nil {
+ return []*errors.ValidationError{errors.InvalidXmlParsing(err.Error(), stringedBody)}
}
+
+ response.Body = io.NopCloser(bytes.NewBuffer(transformedBytes))
}
}
+
+ // Validate response schema
+ valid, vErrs := ValidateResponseSchema(&ValidateResponseSchemaInput{
+ Request: request,
+ Response: response,
+ Schema: schema,
+ Version: helpers.VersionToFloat(v.document.Version),
+ Options: []config.Option{config.WithExistingOpts(v.options)},
+ })
+
+ if !valid {
+ validationErrors = append(validationErrors, vErrs...)
+ }
+
return validationErrors
}
diff --git a/responses/validate_body_test.go b/responses/validate_body_test.go
index a40bdd1..5d9a00f 100644
--- a/responses/validate_body_test.go
+++ b/responses/validate_body_test.go
@@ -1593,6 +1593,208 @@ paths:
assert.Len(t, errs, 0)
}
+func TestValidateBody_ValidXmlDecode(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ responses:
+ default:
+ content:
+ application/xml:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := "test2"
+
+ // build a request
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body)))
+ request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)
+
+ // simulate a request/response
+ res := httptest.NewRecorder()
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(helpers.ContentTypeHeader, "application/xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(body))
+ }
+
+ // fire the request
+ handler(res, request)
+
+ // record response
+ response := res.Result()
+
+ // validate!
+ valid, errors := v.ValidateResponseBody(request, response)
+
+ assert.True(t, valid)
+ assert.Len(t, errors, 0)
+}
+
+func TestValidateBody_ValidXmlFailedValidation(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ responses:
+ default:
+ content:
+ application/xml:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := "20text"
+
+ // build a request
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body)))
+ request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)
+
+ // simulate a request/response
+ res := httptest.NewRecorder()
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(helpers.ContentTypeHeader, "application/xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(body))
+ }
+
+ // fire the request
+ handler(res, request)
+
+ // record response
+ response := res.Result()
+
+ // validate!
+ valid, errors := v.ValidateResponseBody(request, response)
+
+ assert.False(t, valid)
+ assert.Len(t, errors, 1)
+ assert.Len(t, errors[0].SchemaValidationErrors, 1)
+}
+
+func TestValidateBody_IgnoreXmlValidation(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ responses:
+ default:
+ content:
+ application/xml:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer
+ vegetarian:
+ type: boolean`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewResponseBodyValidator(&m.Model)
+
+ body := "invalidbodycausenoxml"
+
+ // build a request
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body)))
+ request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)
+
+ // simulate a request/response
+ res := httptest.NewRecorder()
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(helpers.ContentTypeHeader, "application/xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(body))
+ }
+
+ // fire the request
+ handler(res, request)
+
+ // record response
+ response := res.Result()
+
+ // validate!
+ valid, errors := v.ValidateResponseBody(request, response)
+
+ assert.True(t, valid)
+ assert.Len(t, errors, 0)
+}
+
+func TestValidateBody_InvalidXmlParse(t *testing.T) {
+ spec := `openapi: 3.1.0
+paths:
+ /burgers/createBurger:
+ post:
+ responses:
+ default:
+ content:
+ application/xml:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ patties:
+ type: integer
+ vegetarian:
+ type: boolean`
+
+ doc, _ := libopenapi.NewDocument([]byte(spec))
+
+ m, _ := doc.BuildV3Model()
+ v := NewResponseBodyValidator(&m.Model, config.WithXmlBodyValidation())
+
+ body := ""
+
+ // build a request
+ request, _ := http.NewRequest(http.MethodPost, "https://things.com/burgers/createBurger", bytes.NewReader([]byte(body)))
+ request.Header.Set(helpers.ContentTypeHeader, helpers.JSONContentType)
+
+ // simulate a request/response
+ res := httptest.NewRecorder()
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(helpers.ContentTypeHeader, "application/xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(body))
+ }
+
+ // fire the request
+ handler(res, request)
+
+ // record response
+ response := res.Result()
+
+ // validate!
+ valid, errors := v.ValidateResponseBody(request, response)
+
+ assert.False(t, valid)
+ assert.Len(t, errors, 1)
+ assert.Equal(t, "xml example is malformed", errors[0].Message)
+}
+
type errorReader struct{}
func (er *errorReader) Read(p []byte) (n int, err error) {
diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go
index 5740f1e..15942e4 100644
--- a/schema_validation/validate_schema.go
+++ b/schema_validation/validate_schema.go
@@ -1,6 +1,5 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
-
package schema_validation
import (
diff --git a/schema_validation/validate_xml.go b/schema_validation/validate_xml.go
index ad30bd8..8df685f 100644
--- a/schema_validation/validate_xml.go
+++ b/schema_validation/validate_xml.go
@@ -1,12 +1,12 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
-
package schema_validation
import (
"encoding/json"
"fmt"
"log/slog"
+ "strconv"
"strings"
"github.com/pb33f/libopenapi/datamodel/high/base"
@@ -18,86 +18,250 @@ import (
)
func (x *xmlValidator) validateXMLWithVersion(schema *base.Schema, xmlString string, log *slog.Logger, version float32) (bool, []*liberrors.ValidationError) {
- var validationErrors []*liberrors.ValidationError
-
if schema == nil {
log.Info("schema is empty and cannot be validated")
- return false, validationErrors
+ return false, nil
}
// parse xml and transform to json structure matching schema
- transformedJSON, err := transformXMLToSchemaJSON(xmlString, schema)
- if err != nil {
- violation := &liberrors.SchemaValidationFailure{
- Reason: err.Error(),
- Location: "xml parsing",
- ReferenceSchema: "",
- ReferenceObject: xmlString,
- }
- validationErrors = append(validationErrors, &liberrors.ValidationError{
- ValidationType: helpers.RequestBodyValidation,
- ValidationSubType: helpers.Schema,
- Message: "xml example is malformed",
- Reason: fmt.Sprintf("failed to parse xml: %s", err.Error()),
- SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
- HowToFix: "ensure xml is well-formed and matches schema structure",
- })
- return false, validationErrors
+ transformedJSON, prevalidationErrors := TransformXMLToSchemaJSON(xmlString, schema)
+ if len(prevalidationErrors) > 0 {
+ return false, prevalidationErrors
}
// validate transformed json against schema using existing validator
return x.schemaValidator.validateSchemaWithVersion(schema, nil, transformedJSON, log, version)
}
-// transformXMLToSchemaJSON converts xml to json structure matching openapi schema.
+// TransformXMLToSchemaJSON converts xml to json structure matching openapi schema.
// applies xml object transformations: name, attribute, wrapped.
-func transformXMLToSchemaJSON(xmlString string, schema *base.Schema) (interface{}, error) {
+func TransformXMLToSchemaJSON(xmlString string, schema *base.Schema) (any, []*liberrors.ValidationError) {
if xmlString == "" {
- return nil, fmt.Errorf("empty xml content")
+ return nil, []*liberrors.ValidationError{liberrors.InvalidXmlParsing("empty xml content", xmlString)}
}
- // parse xml using goxml2json with type conversion for numbers only
- // note: we convert floats and ints, but not booleans, since xml content
- // may legitimately contain "true"/"false" as string values
- jsonBuf, err := xj.Convert(strings.NewReader(xmlString), xj.WithTypeConverter(xj.Float, xj.Int))
+ // parse xml using goxml2json. we convert types manually
+ jsonBuf, err := xj.Convert(strings.NewReader(xmlString))
if err != nil {
- return nil, fmt.Errorf("malformed xml: %w", err)
+ return nil, []*liberrors.ValidationError{liberrors.InvalidXmlParsing(fmt.Sprintf("malformed xml: %s", err.Error()), xmlString)}
+ }
+
+ jsonBytes := jsonBuf.Bytes()
+
+ // the smallest valid XML possible "" generates a 10 bytes buffer.
+ // any other invalid XML generates a smaller buffer
+ if len(jsonBytes) < 10 {
+ return nil, []*liberrors.ValidationError{liberrors.InvalidXmlParsing("malformed xml", xmlString)}
}
- // decode to interface{}
- var rawJSON interface{}
- if err := json.Unmarshal(jsonBuf.Bytes(), &rawJSON); err != nil {
- return nil, fmt.Errorf("failed to decode json: %w", err)
+ var rawJSON any
+ if err := json.Unmarshal(jsonBytes, &rawJSON); err != nil {
+ return nil, []*liberrors.ValidationError{liberrors.InvalidXmlParsing(fmt.Sprintf("failed to decode converted xml to json: %s", err.Error()), xmlString)}
}
+ xmlNsMap := make(map[string]string, 2)
+
// apply openapi xml object transformations
- transformed := applyXMLTransformations(rawJSON, schema)
- return transformed, nil
+ return applyXMLTransformations(rawJSON, schema, &xmlNsMap)
+}
+
+func validateXmlNs(dataMap *map[string]any, schema *base.Schema, propName string, xmlNsMap *map[string]string) []*liberrors.ValidationError {
+ var validationErrors []*liberrors.ValidationError
+
+ if dataMap == nil || schema == nil || xmlNsMap == nil {
+ return validationErrors
+ }
+
+ if propName != "" {
+ if val, exists := (*dataMap)[propName]; exists {
+ if converted, ok := val.(map[string]any); ok {
+ dataMap = &converted
+ }
+ }
+ }
+
+ if schema.XML.Prefix != "" {
+ attrKey := "-" + schema.XML.Prefix
+
+ val, exists := (*dataMap)[attrKey]
+
+ if exists {
+ if ns, ok := val.(string); ok {
+ (*xmlNsMap)[schema.XML.Prefix] = ns
+ (*xmlNsMap)[ns] = schema.XML.Prefix
+
+ if schema.XML.Namespace != "" && schema.XML.Namespace != ns {
+ validationErrors = append(validationErrors,
+ liberrors.InvalidNamespace(schema, ns, schema.XML.Namespace, schema.XML.Prefix))
+ }
+ }
+
+ delete((*dataMap), attrKey)
+ } else {
+ validationErrors = append(validationErrors, liberrors.MissingPrefix(schema, schema.XML.Prefix))
+ }
+ }
+
+ if schema.XML.Namespace != "" {
+ _, exists := (*xmlNsMap)[schema.XML.Namespace]
+
+ if !exists {
+ validationErrors = append(validationErrors, liberrors.MissingNamespace(schema, schema.XML.Namespace))
+ }
+ }
+
+ return validationErrors
+}
+
+func convertBasedOnSchema(propName, xmlName string, propValue any, schema *base.Schema, xmlNsMap *map[string]string) (any, []*liberrors.ValidationError) {
+ var xmlNsErrors []*liberrors.ValidationError
+
+ types := schema.Type
+
+ extractTypes := func(proxies []*base.SchemaProxy) {
+ for _, proxy := range proxies {
+ sch := proxy.Schema()
+ if len(sch.Type) > 0 {
+ types = append(types, sch.Type...)
+ }
+ }
+ }
+
+ extractTypes(schema.AllOf)
+ extractTypes(schema.OneOf)
+ extractTypes(schema.AnyOf)
+
+ convertedValue := propValue
+
+typesLoop:
+ for _, pType := range types {
+ // because in XML everything is a string, we try to convert the value to the
+ // actual expected type, so the normal schema validation should pass with correct types
+ switch pType {
+ case helpers.Integer:
+ textValue, isString := propValue.(string)
+
+ if isString {
+ converted, err := strconv.ParseInt(textValue, 10, 64)
+
+ if err == nil {
+ convertedValue = converted
+ break typesLoop
+ }
+ }
+ case helpers.Number:
+ textValue, isString := propValue.(string)
+
+ if isString {
+ converted, err := strconv.ParseFloat(textValue, 64)
+ if err == nil {
+ convertedValue = converted
+ break typesLoop
+ }
+ }
+
+ case helpers.Boolean:
+ textValue, isString := propValue.(string)
+
+ if isString {
+ converted, err := strconv.ParseBool(textValue)
+ if err == nil {
+ convertedValue = converted
+ break typesLoop
+ }
+ }
+
+ case helpers.Array:
+ convertedValue = propValue
+
+ if schema.XML != nil && schema.XML.Wrapped {
+ convertedValue = unwrapArrayElement(propValue, propName, schema)
+ }
+
+ if schema.Items != nil && schema.Items.A != nil {
+ itemSchema := schema.Items.A.Schema()
+
+ arr, isArr := convertedValue.([]any)
+
+ if !isArr {
+ arr = []any{
+ convertedValue,
+ }
+ }
+
+ for index, item := range arr {
+ converted, errs := convertBasedOnSchema(propName, xmlName, item, itemSchema, xmlNsMap)
+
+ if len(errs) > 0 {
+ xmlNsErrors = append(xmlNsErrors, errs...)
+ }
+
+ arr[index] = converted
+ }
+
+ convertedValue = arr
+ break typesLoop
+ }
+ case helpers.Object:
+ objectValue, isObject := propValue.(map[string]any)
+
+ if isObject {
+ newValue, xmlErrors := applyXMLTransformations(objectValue, schema, xmlNsMap)
+
+ if len(xmlErrors) > 0 {
+ xmlNsErrors = append(xmlNsErrors, xmlErrors...)
+ continue typesLoop
+ }
+
+ convertedValue = newValue
+ break typesLoop
+ }
+ }
+ }
+
+ return convertedValue, xmlNsErrors
}
// applyXMLTransformations applies openapi xml object rules to match json schema.
-// handles xml.name (root unwrapping), xml.attribute (dash prefix), xml.wrapped (array unwrapping).
-func applyXMLTransformations(data interface{}, schema *base.Schema) interface{} {
- if schema == nil {
- return data
+// handles xml.name (root unwrapping), xml.attribute (dash prefix), xml.wrapped (array unwrapping),
+// xml.prefix (check existance), xml.namespace (check if exists and match).
+// we delete all attributes, prefixes, and namespaces found in the data interface; therefore, undeclared items
+// are sent in the body for validation, so that 'additionalProperties: false' can detect it.
+func applyXMLTransformations(data any, schema *base.Schema, xmlNsMap *map[string]string) (any, []*liberrors.ValidationError) {
+ if schema == nil || data == nil || xmlNsMap == nil {
+ return data, nil
}
// unwrap root element if xml.name is set on schema
if schema.XML != nil && schema.XML.Name != "" {
- if dataMap, ok := data.(map[string]interface{}); ok {
+ if dataMap, ok := data.(map[string]any); ok {
if wrapped, exists := dataMap[schema.XML.Name]; exists {
data = wrapped
}
}
}
+ var xmlNsErrors []*liberrors.ValidationError
+
// transform properties based on their xml configurations
- if dataMap, ok := data.(map[string]interface{}); ok {
+ if dataMap, ok := data.(map[string]any); ok {
if schema.Properties == nil || schema.Properties.Len() == 0 {
- return data
- }
+ if schema.XML != nil && (schema.XML.Prefix != "" || schema.XML.Namespace != "") {
+ namespaceErrors := validateXmlNs(&dataMap, schema, "", xmlNsMap)
- transformed := make(map[string]interface{}, schema.Properties.Len())
+ if len(namespaceErrors) > 0 {
+ xmlNsErrors = append(xmlNsErrors, namespaceErrors...)
+ } else {
+ if content, has := dataMap["#content"]; has {
+ if stringContent, ok := content.(string); ok {
+ data = stringContent
+ }
+ }
+ }
+ }
+
+ return data, xmlNsErrors
+ }
for pair := schema.Properties.First(); pair != nil; pair = pair.Next() {
propName := pair.Key()
@@ -107,49 +271,76 @@ func applyXMLTransformations(data interface{}, schema *base.Schema) interface{}
continue
}
- // determine xml element name (defaults to property name)
xmlName := propName
- if propSchema.XML != nil && propSchema.XML.Name != "" {
- xmlName = propSchema.XML.Name
+
+ if propSchema.XML != nil {
+ // determine xml element name (defaults to property name)
+ if propSchema.XML.Name != "" {
+ xmlName = propSchema.XML.Name
+ }
}
- // handle xml.attribute: true - attributes are prefixed with dash
- if propSchema.XML != nil && propSchema.XML.Attribute {
- attrKey := "-" + xmlName
- if val, exists := dataMap[attrKey]; exists {
- transformed[propName] = val
- continue
+ if propSchema.XML != nil {
+ namespaceErrors := validateXmlNs(&dataMap, propSchema, xmlName, xmlNsMap)
+
+ if len(namespaceErrors) > 0 {
+ xmlNsErrors = append(xmlNsErrors, namespaceErrors...)
+ }
+
+ // handle xml.attribute: true - attributes are prefixed with dash
+ if propSchema.XML.Attribute {
+ attrKey := "-" + xmlName
+ if val, exists := dataMap[attrKey]; exists {
+ // If the value is an attribute, it cannot have a namespace
+ convertedValue, _ := convertBasedOnSchema(propName, xmlName, val, propSchema, xmlNsMap)
+ dataMap[propName] = convertedValue
+ delete(dataMap, attrKey)
+ continue
+ }
}
}
// handle regular elements
if val, exists := dataMap[xmlName]; exists {
- // handle wrapped arrays: unwrap container element
- if len(propSchema.Type) > 0 && propSchema.Type[0] == "array" &&
- propSchema.XML != nil && propSchema.XML.Wrapped {
- val = unwrapArrayElement(val, propSchema)
+ if mapObject, ok := val.(map[string]any); ok {
+ if content, has := mapObject["#content"]; has {
+ if stringContent, ok := content.(string); ok {
+ val = stringContent
+ }
+ }
+ }
+
+ convertedValue, nsErrors := convertBasedOnSchema(propName, xmlName, val, propSchema, xmlNsMap)
+
+ if len(nsErrors) > 0 {
+ xmlNsErrors = append(xmlNsErrors, nsErrors...)
}
- transformed[propName] = val
+ dataMap[propName] = convertedValue
+
+ if propName != xmlName {
+ delete(dataMap, xmlName)
+ }
}
}
-
- return transformed
}
- return data
+ return data, xmlNsErrors
}
// unwrapArrayElement removes wrapping element from xml arrays when xml.wrapped is true.
// example: {"items": {"item": [...]}} becomes [...]
-func unwrapArrayElement(val interface{}, propSchema *base.Schema) interface{} {
- wrapMap, ok := val.(map[string]interface{})
+func unwrapArrayElement(val any, itemName string, propSchema *base.Schema) any {
+ wrapMap, ok := val.(map[string]any)
if !ok {
return val
}
+ if propSchema.XML.Name != "" {
+ itemName = propSchema.XML.Name
+ }
+
// determine item element name
- itemName := "item"
if propSchema.Items != nil && propSchema.Items.A != nil {
itemSchema := propSchema.Items.A.Schema()
if itemSchema != nil && itemSchema.XML != nil && itemSchema.XML.Name != "" {
diff --git a/schema_validation/validate_xml_test.go b/schema_validation/validate_xml_test.go
index 1d3faad..29e3c42 100644
--- a/schema_validation/validate_xml_test.go
+++ b/schema_validation/validate_xml_test.go
@@ -1,12 +1,12 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
-
package schema_validation
import (
"testing"
"github.com/pb33f/libopenapi"
+ "github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/stretchr/testify/assert"
)
@@ -751,15 +751,16 @@ func TestValidateXML_NilSchema(t *testing.T) {
func TestValidateXML_NilSchemaInTransformation(t *testing.T) {
// directly test applyXMLTransformations with nil schema (line 94)
- result := applyXMLTransformations(map[string]interface{}{"test": "value"}, nil)
+ xmlNsMap := make(map[string]string, 2)
+ result, err := applyXMLTransformations(map[string]interface{}{"test": "value"}, nil, &xmlNsMap)
assert.NotNil(t, result)
+ assert.Len(t, err, 0)
assert.Equal(t, map[string]interface{}{"test": "value"}, result)
}
func TestValidateXML_TransformWithNilPropertySchemaProxy(t *testing.T) {
// directly test applyXMLTransformations when a property schema proxy returns nil (line 119)
// this can happen with circular refs or unresolved refs in edge cases
-
// create a schema with properties but we'll simulate a nil schema scenario
// by testing the transformation directly
data := map[string]interface{}{
@@ -770,8 +771,9 @@ func TestValidateXML_TransformWithNilPropertySchemaProxy(t *testing.T) {
schema := &base.Schema{
Properties: nil, // will trigger line 109 early return
}
-
- result := applyXMLTransformations(data, schema)
+ xmlNsMap := make(map[string]string, 2)
+ result, err := applyXMLTransformations(data, schema, &xmlNsMap)
+ assert.Len(t, err, 0)
assert.Equal(t, data, result)
}
@@ -891,6 +893,7 @@ paths:
xml:
wrapped: true
items:
+ additionalProperties: false
type: object
properties:
value:
@@ -914,11 +917,12 @@ paths:
// wrapper contains items with wrong name (item instead of record)
// this tests the fallback path where unwrapped element is not found
xmlWithWrongItemName := `- test
`
- valid, validationErrors := validator.ValidateXMLString(schema, xmlWithWrongItemName)
+ valid, _ := validator.ValidateXMLString(schema, xmlWithWrongItemName)
+ assert.False(t, valid)
- // it should still process (might fail schema validation but won't crash)
- _ = valid
- assert.NotNil(t, validationErrors)
+ xmlWithWrightItemName := `test`
+ valid, _ = validator.ValidateXMLString(schema, xmlWithWrightItemName)
+ assert.True(t, valid)
}
func TestValidateXML_DirectArrayValue(t *testing.T) {
@@ -935,7 +939,7 @@ func TestValidateXML_DirectArrayValue(t *testing.T) {
// when val is already an array (not a map), it should return as-is
arrayVal := []interface{}{"one", "two", "three"}
- result := unwrapArrayElement(arrayVal, schema)
+ result := unwrapArrayElement(arrayVal, "", schema)
assert.Equal(t, arrayVal, result)
}
@@ -953,16 +957,16 @@ func TestValidateXML_UnwrapArrayElementMissingItem(t *testing.T) {
// wrapper map contains wrong key - should return map as-is (line 177)
wrapperMap := map[string]interface{}{"wrongKey": []interface{}{"one", "two"}}
- result := unwrapArrayElement(wrapperMap, schema)
+ result := unwrapArrayElement(wrapperMap, "", schema)
assert.Equal(t, wrapperMap, result)
}
func TestTransformXMLToSchemaJSON_EmptyString(t *testing.T) {
// test empty string error path (line 68)
schema := &base.Schema{}
- _, err := transformXMLToSchemaJSON("", schema)
- assert.Error(t, err)
- assert.Contains(t, err.Error(), "empty xml")
+ _, err := TransformXMLToSchemaJSON("", schema)
+ assert.Len(t, err, 1)
+ assert.Contains(t, err[0].Reason, "empty xml content")
}
func TestApplyXMLTransformations_NoXMLName(t *testing.T) {
@@ -970,8 +974,10 @@ func TestApplyXMLTransformations_NoXMLName(t *testing.T) {
schema := &base.Schema{
Properties: nil,
}
+ xmlNsMap := make(map[string]string, 2)
data := map[string]interface{}{"Cat": map[string]interface{}{"nice": "true"}}
- result := applyXMLTransformations(data, schema)
+ result, err := applyXMLTransformations(data, schema, &xmlNsMap)
+ assert.Len(t, err, 0)
assert.Equal(t, data, result)
}
@@ -997,3 +1003,232 @@ func TestIsXMLContentType(t *testing.T) {
})
}
}
+
+func TestTransformXMLToSchemaJSON_InvalixXml(t *testing.T) {
+ schema := &base.Schema{}
+ _, err := TransformXMLToSchemaJSON("<", schema)
+ assert.Len(t, err, 1)
+ assert.Contains(t, err[0].Reason, "malformed xml")
+}
+
+func TestValidateXmlNs_NoData(t *testing.T) {
+ errors := validateXmlNs(nil, nil, "", nil)
+ assert.Len(t, errors, 0)
+}
+
+func getXmlTestSchema(t *testing.T) *base.Schema {
+ spec := `openapi: 3.1
+paths:
+ /collection:
+ get:
+ responses:
+ '200':
+ content:
+ application/xml:
+ schema:
+ type: object
+ additionalProperties: false
+ properties:
+ body:
+ type: object
+ required:
+ - id
+ - success
+ - payload
+ xml:
+ prefix: t
+ namespace: http://assert.t
+ name: reqBody
+ properties:
+ id:
+ type: integer
+ xml:
+ attribute: true
+ success:
+ xml:
+ name: ok
+ prefix: j
+ namespace: http://j.j
+ type: boolean
+ payload:
+ oneOf:
+ - type: integer
+ - type: object
+ data:
+ type: array
+ xml:
+ wrapped: true
+ name: list
+ items:
+ additionalProperties: false
+ type: object
+ required:
+ - value
+ properties:
+ value:
+ type: string
+ xml:
+ namespace: http://prop.arr
+ prefix: arr
+ xml:
+ name: record
+ prefix: unt
+ namespace: http://expect.t
+ xml:
+ name: Collection`
+
+ doc, err := libopenapi.NewDocument([]byte(spec))
+ assert.NoError(t, err)
+
+ v3Doc, err := doc.BuildV3Model()
+ assert.NoError(t, err)
+
+ schema := v3Doc.Model.Paths.PathItems.GetOrZero("/collection").Get.Responses.Codes.GetOrZero("200").
+ Content.GetOrZero("application/xml").Schema.Schema()
+
+ return schema
+}
+
+func TestValidateXmlNs_InvalidPrefix(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+ xmlPayload := ``
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType)
+}
+
+func TestValidateXmlNs_InvalidNamespace(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+ xmlPayload := ``
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, helpers.XmlValidationNamespace, err[0].ValidationSubType)
+}
+
+func TestValidateXmlNs_InvalidNamespaceInRoot(t *testing.T) {
+ spec := `openapi: 3.0.0
+paths:
+ /pet:
+ get:
+ responses:
+ '200':
+ content:
+ application/xml:
+ schema:
+ type: object
+ xml:
+ name: Cat
+ prefix: c
+ namespace: http://cat.ca`
+
+ doc, err := libopenapi.NewDocument([]byte(spec))
+ assert.NoError(t, err)
+
+ v3Doc, err := doc.BuildV3Model()
+ assert.NoError(t, err)
+
+ schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200").
+ Content.GetOrZero("application/xml").Schema.Schema()
+
+ validator := NewXMLValidator()
+ xmlPayload := ``
+
+ valid, validationErrors := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, "The namespace from prefix 'c' differs from the xml", validationErrors[0].Message)
+ assert.Equal(t, helpers.XmlValidationNamespace, validationErrors[0].ValidationSubType)
+}
+
+func TestValidateXmlNs_CorrectNamespaceInRoot(t *testing.T) {
+ spec := `openapi: 3.0.0
+paths:
+ /pet:
+ get:
+ responses:
+ '200':
+ content:
+ application/xml:
+ schema:
+ type: string
+ xml:
+ name: Cat
+ prefix: c
+ namespace: http://cat.ca`
+
+ doc, err := libopenapi.NewDocument([]byte(spec))
+ assert.NoError(t, err)
+
+ v3Doc, err := doc.BuildV3Model()
+ assert.NoError(t, err)
+
+ schema := v3Doc.Model.Paths.PathItems.GetOrZero("/pet").Get.Responses.Codes.GetOrZero("200").
+ Content.GetOrZero("application/xml").Schema.Schema()
+
+ validator := NewXMLValidator()
+ xmlPayload := `meow`
+
+ valid, validationErrors := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.True(t, valid)
+ assert.Len(t, validationErrors, 0)
+}
+
+func TestConvertBasedOnSchema_XmlSuccessfullyConverted(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+
+ xmlPayload := `true2
+Text
`
+
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.True(t, valid)
+ assert.Len(t, err, 0)
+}
+
+func TestConvertBasedOnSchema_MissingPrefixInObjectProperties(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+
+ xmlPayload := `true2
+Text
`
+
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType)
+ assert.Equal(t, "The prefix 'j' is defined in the schema, however it's missing from the xml", err[0].Message)
+}
+
+func TestConvertBasedOnSchema_MissingPrefixInArrayItemProperties(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+
+ xmlPayload := `true2
+Text
`
+
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, helpers.XmlValidationPrefix, err[0].ValidationSubType)
+ assert.Equal(t, "The prefix 'arr' is defined in the schema, however it's missing from the xml", err[0].Message)
+}
+
+func TestApplyXMLTransformations_IncorrectSchema(t *testing.T) {
+ schema := getXmlTestSchema(t)
+ validator := NewXMLValidator()
+
+ xmlPayload := `NotBooleanNotInteger
+Text
`
+
+ valid, err := validator.ValidateXMLString(schema, xmlPayload)
+
+ assert.False(t, valid)
+ assert.Equal(t, "got string, want boolean", err[0].SchemaValidationErrors[0].Reason)
+ assert.Equal(t, "schema does not pass validation", err[0].Message)
+}