Skip to content
Merged
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
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
[![discord](https://img.shields.io/discord/923258363540815912)](https://discord.gg/x7VACVuEGP)
[![Docs](https://img.shields.io/badge/godoc-reference-5fafd7)](https://pkg.go.dev/github.com/pb33f/libopenapi-validator)

A validation module for [libopenapi](https://github.com/pb33f/libopenapi).
A validation module for [libopenapi](https://github.com/pb33f/libopenapi).

Document validation is explicitly covered for OpenAPI 3.0, 3.1, and 3.2.

`libopenapi-validator` will validate the following elements against an OpenAPI 3+ specification

Expand All @@ -18,7 +20,24 @@ A validation module for [libopenapi](https://github.com/pb33f/libopenapi).
- *libopenapi.Document* - Validates the OpenAPI document against the OpenAPI specification
- *base.Schema* - Validates a schema against a JSON or YAML blob / unmarshalled object

👉👉 [Check out the full documentation](https://pb33f.io/libopenapi/validation/) 👈👈
👉👉 [Check out the full documentation](https://pb33f.io/libopenapi/validation/) 👈👈

## Runtime validation features

The validator now includes a standalone OpenAPI router, authentication callbacks with route context and replayable bodies, immutable custom body codecs, opt-in request defaults, `Parameter.content` decoding, validation policy switches, and explicit OpenAPI 3.2 document validation.

Existing behavior remains the default. Strict server matching, non-JSON codecs, request mutation, and new rejection policies are opt-in:

```go
v := validator.NewValidatorFromV3Model(&model.Model,
config.WithStrictServerMatching(),
config.WithStandardBodyDecoders(),
config.WithRejectUnsupportedBodyContent(),
config.WithRequestDefaults(),
)
```

Use `router.NewRouter` for routing without validation, `WithBodyDecoder` and `WithBodyEncoder` for proprietary formats, and `WithAuthenticationFunc` for application-owned security checks. See the [complete validation guide](https://pb33f.io/libopenapi/validation/) for defaults, options, and examples.

---

Expand Down
224 changes: 202 additions & 22 deletions config/config.go

Large diffs are not rendered by default.

130 changes: 127 additions & 3 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import (
"sync"
"testing"

validatorcache "github.com/pb33f/libopenapi-validator/cache"
"github.com/pb33f/libopenapi-validator/radix"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/testify/assert"
"github.com/pb33f/testify/require"
"github.com/santhosh-tekuri/jsonschema/v6"

v3 "github.com/pb33f/libopenapi/datamodel/high/v3"

validatorcache "github.com/pb33f/libopenapi-validator/cache"
"github.com/pb33f/libopenapi-validator/content"
"github.com/pb33f/libopenapi-validator/radix"
)

func TestNewValidationOptions_Defaults(t *testing.T) {
Expand All @@ -27,6 +32,7 @@ func TestNewValidationOptions_Defaults(t *testing.T) {
assert.False(t, opts.AllowScalarCoercion) // Default is false
assert.False(t, opts.AllowXMLBodyValidation) // Default is false
assert.False(t, opts.AllowURLEncodedBodyValidation) // Default is false
assert.False(t, opts.ValidateContentParameters)
assert.Nil(t, opts.RegexEngine)
assert.Nil(t, opts.RegexCache)
assert.NotNil(t, opts.SchemaCache)
Expand Down Expand Up @@ -368,6 +374,124 @@ func TestWithExistingOpts_AuthenticationFuncCopied(t *testing.T) {
assert.True(t, called)
}

func TestBodyCodecAndParityOptions(t *testing.T) {
decoder := content.DecoderFunc(func(*content.DecodeInput) (any, error) { return "custom", nil })
encoder := content.EncoderFunc(func(*content.EncodeInput) ([]byte, error) { return []byte("custom"), nil })
zipLimits := content.ZipLimits{CompressedSize: 1024, ExpandedSize: 2048, Entries: 4, ExpansionRatio: 10}
opts := NewValidationOptions(
WithBodyDecoder("application/custom", decoder),
WithBodyEncoder("application/custom", encoder),
WithStandardBodyDecoders(),
WithZipBodyDecoder(zipLimits),
WithRejectUnsupportedBodyContent(),
WithRejectUndeclaredRequestBody(),
WithoutRequestQueryParameterValidation(),
WithoutRequestBodyValidation(),
WithoutResponseBodyValidation(),
WithoutResponseStatusValidation(),
WithRequestDefaults(),
WithStrictServerMatching(),
)
assert.True(t, opts.RejectUnsupportedBodyContent)
assert.True(t, opts.RejectUndeclaredRequestBody)
assert.False(t, opts.ValidateRequestQuery)
assert.False(t, opts.ValidateRequestBody)
assert.False(t, opts.ValidateResponseBody)
assert.False(t, opts.ValidateResponseStatus)
assert.True(t, opts.RequestDefaults)
assert.True(t, opts.StrictServerMatching)
resolved, _, _ := opts.BodyRegistry.Decoder("application/custom")
require.NotNil(t, resolved)
value, err := resolved.Decode(nil)
require.NoError(t, err)
assert.Equal(t, "custom", value)
resolvedEncoder, _, _ := opts.BodyRegistry.Encoder("application/custom")
require.NotNil(t, resolvedEncoder)
encoded, err := resolvedEncoder.Encode(nil)
require.NoError(t, err)
assert.Equal(t, "custom", string(encoded))
for _, mediaType := range []string{"application/yaml", "application/xml", "multipart/form-data", "text/plain", "application/zip"} {
resolved, _, _ = opts.BodyRegistry.Decoder(mediaType)
assert.NotNil(t, resolved, mediaType)
}
}

func TestCompatibilityCodecAliasesAndContentParameterDecoder(t *testing.T) {
parameterDecoder := func(context.Context, *ContentParameterInput) (any, *base.Schema, error) {
return "value", nil, nil
}
opts := NewValidationOptions(WithXmlBodyValidation(), WithURLEncodedBodyValidation(), WithContentParameterDecoder(parameterDecoder))
assert.True(t, opts.AllowXMLBodyValidation)
assert.True(t, opts.AllowURLEncodedBodyValidation)
assert.NotNil(t, opts.ContentParameterDecoder)
assert.True(t, opts.ValidateContentParameters)
for _, mediaType := range []string{"application/xml", "text/xml", "application/x-www-form-urlencoded"} {
decoder, _, _ := opts.BodyRegistry.Decoder(mediaType)
assert.NotNil(t, decoder, mediaType)
}
}

func TestContentParameterValidationOptionsAndDecoderPrecedence(t *testing.T) {
opts := NewValidationOptions(WithContentParameterValidation())
assert.True(t, opts.ValidateContentParameters)
assert.Nil(t, opts.ContentParameterDecoder)
opts = NewValidationOptions(WithContentParameterDecoder(nil))
assert.False(t, opts.ValidateContentParameters)

opts = NewValidationOptions(WithXmlBodyValidation(), WithStandardBodyDecoders())
_, genericWins := opts.BodyRegistry.ExactDecoder("application/xml").(content.CompatibilityDecoder)
assert.False(t, genericWins)
opts = NewValidationOptions(WithStandardBodyDecoders(), WithXmlBodyValidation())
marker, compatibilityWins := opts.BodyRegistry.ExactDecoder("application/xml").(content.CompatibilityDecoder)
require.True(t, compatibilityWins)
assert.Equal(t, "xml", marker.CompatibilityKind())
}

func TestWithExistingOptsCopiesParityStateAndRegistry(t *testing.T) {
decoder := content.DecoderFunc(func(*content.DecodeInput) (any, error) { return true, nil })
parameterDecoder := func(context.Context, *ContentParameterInput) (any, *base.Schema, error) { return nil, nil, nil }
original := NewValidationOptions(
WithBodyDecoder("application/custom", decoder), WithRejectUnsupportedBodyContent(),
WithRejectUndeclaredRequestBody(), WithoutRequestQueryParameterValidation(), WithoutRequestBodyValidation(),
WithoutResponseBodyValidation(), WithoutResponseStatusValidation(), WithRequestDefaults(),
WithContentParameterDecoder(parameterDecoder),
WithStrictServerMatching(),
)
copied := NewValidationOptions(WithExistingOpts(original))
assert.Same(t, original.BodyRegistry, copied.BodyRegistry)
assert.Equal(t, original.RejectUnsupportedBodyContent, copied.RejectUnsupportedBodyContent)
assert.Equal(t, original.RejectUndeclaredRequestBody, copied.RejectUndeclaredRequestBody)
assert.Equal(t, original.ValidateRequestQuery, copied.ValidateRequestQuery)
assert.Equal(t, original.ValidateRequestBody, copied.ValidateRequestBody)
assert.Equal(t, original.ValidateResponseBody, copied.ValidateResponseBody)
assert.Equal(t, original.ValidateResponseStatus, copied.ValidateResponseStatus)
assert.Equal(t, original.RequestDefaults, copied.RequestDefaults)
assert.Equal(t, original.StrictServerMatching, copied.StrictServerMatching)
assert.NotNil(t, copied.ContentParameterDecoder)
assert.True(t, copied.ValidateContentParameters)
original.Release()
assert.Nil(t, original.BodyRegistry)
assert.Nil(t, original.ContentParameterDecoder)
assert.False(t, original.ValidateContentParameters)
}

func TestWithExistingOptsBorrowsSharedReleaseState(t *testing.T) {
schemaCache := validatorcache.NewDefaultCache()
schemaCache.Store(1, &validatorcache.SchemaCacheEntry{RenderedInline: []byte("schema")})
pathTree := radix.NewPathTree()
pathTree.Insert("/pets", &v3.PathItem{})
original := NewValidationOptions(WithSchemaCache(schemaCache), WithPathTree(pathTree))
borrowed := NewValidationOptions(WithExistingOpts(original))
borrowed.Release()
_, found := schemaCache.Load(1)
assert.True(t, found)
assert.Equal(t, 1, pathTree.Size())
original.Release()
_, found = schemaCache.Load(1)
assert.False(t, found)
assert.Equal(t, 0, pathTree.Size())
}

// Tests for new OpenAPI and scalar coercion configuration options

func TestWithOpenAPIMode(t *testing.T) {
Expand Down
Loading
Loading