Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
fb43628
Address https://github.com/daveshanley/vacuum/issues/726
daveshanley Nov 6, 2025
f915f56
tuned up docs a little
daveshanley Nov 6, 2025
6e7612b
bumping coverage
daveshanley Nov 6, 2025
a320efa
more test coverage, reduced complexity a little.
daveshanley Nov 6, 2025
696af4f
Adding XML validation
daveshanley Nov 7, 2025
e260a7e
fixing borked JSON that is now being captured in `libopenapi`
daveshanley Nov 7, 2025
bc1a3a6
added more test coverage.
daveshanley Nov 8, 2025
408847e
updated coverage
daveshanley Nov 8, 2025
9e79377
linting issue. fixed
daveshanley Nov 8, 2025
e868dd6
Refactored out XML validation into a new interface.
daveshanley Nov 8, 2025
bde0446
ran gofumpt
daveshanley Nov 8, 2025
aa3b06b
refactor: remove deprecated Location field from SchemaValidationFailure
Nov 15, 2025
5ff8999
refactor: use centralized JSON Pointer helpers across codebase
Nov 15, 2025
5775925
fix lint and test
Nov 15, 2025
a825714
WIP: Add upstream files and fix Location field references
Nov 15, 2025
d693ff6
Merge origin/main into unify-context-and-centralize
Nov 15, 2025
fabe24d
Fix upstream merge conflicts and update dependencies
Nov 15, 2025
fcb8f64
Fix linting errors
Nov 15, 2025
bf68afe
Fix additional gofumpt formatting issues
Nov 15, 2025
2141779
Remove duplicate OriginalError field
Nov 15, 2025
6013125
Use ValidateSingleParameterSchemaWithPath consistently
Nov 15, 2025
d750797
Use helper for JSON Pointer construction in formatJsonSchemaValidatio…
Nov 15, 2025
b688b24
Bump go.yaml.in/yaml/v4 from 4.0.0-rc.2 to 4.0.0-rc.3
dependabot[bot] Nov 10, 2025
6cb7222
Bump github.com/pb33f/libopenapi from 0.28.1 to 0.28.2
dependabot[bot] Nov 10, 2025
c789944
feat: flag to allow converting from yaml to json pre validation
reversearrow Nov 20, 2025
b33e026
Bump golang.org/x/text from 0.30.0 to 0.31.0
dependabot[bot] Dec 1, 2025
de382bd
bumps deps and uses new context aware rendering.
daveshanley Dec 18, 2025
1d28871
update workflow
daveshanley Dec 18, 2025
3512bfa
fix failing test with missed error handling.
daveshanley Dec 18, 2025
3074e7d
Merge origin/main into unify-context-and-centralize
Dec 30, 2025
026692d
added new strict module
daveshanley Dec 29, 2025
4707bb0
added logger and strict mode config details.
daveshanley Dec 29, 2025
9ad5cef
Added strict errors
daveshanley Dec 29, 2025
7e136ba
added 3.2 to vocab
daveshanley Dec 29, 2025
f4a4e53
add strict mode to params
daveshanley Dec 29, 2025
a467ae1
added strict mode support to responses
daveshanley Dec 29, 2025
9496dd1
added strict mode to requests
daveshanley Dec 29, 2025
bf26bae
cleaned up compiler vocab
daveshanley Dec 29, 2025
c81f22f
validator tests
daveshanley Dec 29, 2025
af96533
flaky test
daveshanley Dec 29, 2025
c8a82f7
Address #210
daveshanley Dec 29, 2025
9d063f3
Address issue #136
daveshanley Dec 29, 2025
f07e8e8
Address issue #181
daveshanley Dec 29, 2025
1b82dcd
Address #183
daveshanley Dec 29, 2025
d4dfa75
fixing more headers
daveshanley Dec 29, 2025
1f49c2c
bump coverage
daveshanley Dec 29, 2025
d579936
Address #184 and #183
daveshanley Dec 29, 2025
4014826
Fix #192
daveshanley Dec 29, 2025
e363c63
fixed #192
daveshanley Dec 29, 2025
1ea0260
bumping coverage
daveshanley Dec 29, 2025
a82f996
more test coverage
daveshanley Dec 29, 2025
ad36920
MOAR COVERAGE
daveshanley Dec 30, 2025
e02e937
bumping coverage
daveshanley Dec 30, 2025
69eff68
fixed inting issues
daveshanley Dec 30, 2025
15a49de
MOAR COVERAGE!
daveshanley Dec 30, 2025
cc5298b
cleaning things up, adding coverage.
daveshanley Dec 30, 2025
aefcffe
continuing the slog up code coverage hill.
daveshanley Dec 30, 2025
19a6145
cleaning up code, fixing test coverage.
daveshanley Dec 30, 2025
6e0edc0
I think this is as high as I can push it (coverage)
daveshanley Dec 30, 2025
88448a4
update libopenapi
daveshanley Dec 30, 2025
f7199c1
use json pointer library escape
Dec 30, 2025
90324ef
Added line / col and security scheme tracking.
daveshanley Dec 31, 2025
f040207
fix test coverage and liniting issues
daveshanley Dec 31, 2025
2327640
Address https://github.com/daveshanley/vacuum/issues/788
daveshanley Jan 6, 2026
cadff75
updated deps
daveshanley Jan 6, 2026
27270d1
signature change on libopenapi
daveshanley Jan 27, 2026
a668af9
nil check on JSON being present.
daveshanley Jan 27, 2026
675f676
update deps
daveshanley Jan 27, 2026
7993ec4
update deps and remove replace directive
daveshanley Jan 27, 2026
ea8e6f5
Fix empty ParameterName
Nivl Jan 17, 2026
545b06c
refactor: add more consts and use them
Nivl Jan 18, 2026
215edc0
fix linter: Stop using RequestMissingOperation
Nivl Jan 18, 2026
788fde5
more ParameterValidationPath changes
Nivl Jan 18, 2026
fba053e
Fix Options During Validator Creation
its-hammer-time Jan 20, 2026
0db7586
Cache YAML Nodes
its-hammer-time Jan 20, 2026
f1dc4fb
Transform 3.0 schema with allOf + nullable into oneOf
alexrjones Feb 3, 2026
ba37a36
gofumpt
alexrjones Feb 3, 2026
85b4a06
bump libopenapi
daveshanley Feb 3, 2026
696dbf8
chore: revamp tests for validate response by using a testbed and a re…
henripqt Feb 3, 2026
80eb95d
chore: add golangci-lint as tool
henripqt Feb 3, 2026
846c366
chore: add pre-commit hook
henripqt Feb 3, 2026
e0d1476
update copyright year
Feb 11, 2026
442023b
merge origin/main and resolve conflicts
Feb 11, 2026
564f49e
cache validation and avoid re-encoding
daveshanley Feb 13, 2026
d8cc0df
error handling coverage
daveshanley Feb 13, 2026
6138997
Bump github.com/pb33f/libopenapi from 0.33.1 to 0.33.7
dependabot[bot] Feb 11, 2026
16629f8
feat: add support for implicit and explicit head response validation
saisatishkarra Feb 11, 2026
6a3bd26
fix: validate errors within head response object
saisatishkarra Feb 13, 2026
d8d2c68
fix: linting issues
saisatishkarra Feb 13, 2026
874503b
Fix: enum with nullable property to automatically include null value …
k2tzumi Feb 14, 2026
2f764db
Add comprehensive test cases for this fix.
k2tzumi Feb 14, 2026
26d43b7
add lazy context tracking.
daveshanley Feb 14, 2026
3c4fb49
add xml conversion to bodies
ySnoopyDogy Jan 8, 2026
4f92af0
better coverage lines
ySnoopyDogy Jan 8, 2026
86a762b
only convert values that needs to
ySnoopyDogy Feb 2, 2026
bdc8f0a
remove unused constants
ySnoopyDogy Feb 2, 2026
5cc2283
bump coverage
ySnoopyDogy Feb 15, 2026
89ccb53
fix lint
ySnoopyDogy Feb 15, 2026
909012a
implement x-www-form-urlencoded validation
ySnoopyDogy Feb 12, 2026
4f0ba57
fix lint issues
ySnoopyDogy Feb 12, 2026
c7e173d
bump error coverage
ySnoopyDogy Feb 15, 2026
cd264e7
support validation for all http schemes
x-delfino Feb 15, 2026
cc122de
Bump github.com/pb33f/libopenapi from 0.33.10 to 0.33.11
dependabot[bot] Feb 16, 2026
b8be188
Bump golang.org/x/text from 0.33.0 to 0.34.0
dependabot[bot] Feb 16, 2026
e7cbb6e
merge origin/main and resolve conflicts
Feb 26, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1
version: v2.7
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: 1.24.7
go-version: 1.25
id: go

- name: Checkout code
Expand Down
2 changes: 0 additions & 2 deletions .gitignore

This file was deleted.

14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:

# Run golangci-lint as a pre-commit hook to catch issues before they are pushed
# See https://golangci-lint.run/ for more information
- repo: local
hooks:
- id: golangci-lint
name: Lint Go code
entry: go tool golangci-lint run
language: system
pass_filenames: false
types: [go]
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,20 @@ go get github.com/pb33f/libopenapi-validator
## Validate OpenAPI Document

```bash
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest [--regexengine] <file>
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest [--regexengine] [--yaml2json] <file>
```

## Install pre-commit hook

To install the pre-commit hook, run the following command in your terminal:

```bash
pre-commit install
```

### Options

#### --regexengine
🔍 Example: Use a custom regex engine/flag (e.g., ecmascript)
```bash
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=ecmascript <file>
Expand All @@ -51,6 +63,26 @@ go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --regexengine=e
- re2
- unicode

#### --yaml2json
🔍 Convert YAML files to JSON before validation (ℹ️ Default: false)

[libopenapi](https://github.com/pb33f/libopenapi/blob/main/datamodel/spec_info.go#L115) passes `map[interface{}]interface{}` structures for deeply nested objects or complex mappings in the OpenAPI specification, which are not allowed in JSON.
These structures cannot be properly converted to JSON by libopenapi and cannot be validated by jsonschema, resulting in ambiguous errors.

This flag allows pre-converting from YAML to JSON to bypass this limitation of the libopenapi.

**When does this happen?**
- OpenAPI specs with deeply nested schema definitions
- Complex `allOf`, `oneOf`, or `anyOf` structures with multiple levels
- Specifications with intricate object mappings in examples or schema properties

Enabling this flag pre-converts the YAML document from YAML to JSON, ensuring a clean JSON structure before validation.

Example:
```bash
go run github.com/pb33f/libopenapi-validator/cmd/validate@latest --yaml2json <file>
```

## Documentation

- [The structure of the validator](https://pb33f.io/libopenapi/validation/#the-structure-of-the-validator)
Expand Down
10 changes: 6 additions & 4 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cache
import (
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/santhosh-tekuri/jsonschema/v6"
"go.yaml.in/yaml/v4"
)

// SchemaCacheEntry holds a compiled schema and its intermediate representations.
Expand All @@ -16,12 +17,13 @@ type SchemaCacheEntry struct {
ReferenceSchema string // String version of RenderedInline
RenderedJSON []byte
CompiledSchema *jsonschema.Schema
RenderedNode *yaml.Node
}

// SchemaCache defines the interface for schema caching implementations.
// The key is a [32]byte hash of the schema (from schema.GoLow().Hash()).
// The key is a uint64 hash of the schema (from schema.GoLow().Hash()).
type SchemaCache interface {
Load(key [32]byte) (*SchemaCacheEntry, bool)
Store(key [32]byte, value *SchemaCacheEntry)
Range(f func(key [32]byte, value *SchemaCacheEntry) bool)
Load(key uint64) (*SchemaCacheEntry, bool)
Store(key uint64, value *SchemaCacheEntry)
Range(f func(key uint64, value *SchemaCacheEntry) bool)
}
56 changes: 23 additions & 33 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ func TestDefaultCache_StoreAndLoad(t *testing.T) {
CompiledSchema: &jsonschema.Schema{},
}

// Create a test key (32-byte hash)
var key [32]byte
copy(key[:], []byte("test-schema-hash-12345678901234"))
// Create a test key (uint64 hash)
key := uint64(0x123456789abcdef0)

// Store the schema
cache.Store(key, testSchema)
Expand All @@ -49,8 +48,7 @@ func TestDefaultCache_LoadMissing(t *testing.T) {
cache := NewDefaultCache()

// Try to load a key that doesn't exist
var key [32]byte
copy(key[:], []byte("nonexistent-key-12345678901234"))
key := uint64(0xdeadbeef)

loaded, ok := cache.Load(key)
assert.False(t, ok, "Should not find non-existent key")
Expand All @@ -60,7 +58,7 @@ func TestDefaultCache_LoadMissing(t *testing.T) {
func TestDefaultCache_LoadNilCache(t *testing.T) {
var cache *DefaultCache

var key [32]byte
key := uint64(0)
loaded, ok := cache.Load(key)

assert.False(t, ok)
Expand All @@ -71,7 +69,7 @@ func TestDefaultCache_StoreNilCache(t *testing.T) {
var cache *DefaultCache

// Should not panic
var key [32]byte
key := uint64(0)
cache.Store(key, &SchemaCacheEntry{})

// Verify nothing was stored (cache is nil)
Expand All @@ -82,10 +80,9 @@ func TestDefaultCache_Range(t *testing.T) {
cache := NewDefaultCache()

// Store multiple entries
entries := make(map[[32]byte]*SchemaCacheEntry)
entries := make(map[uint64]*SchemaCacheEntry)
for i := 0; i < 5; i++ {
var key [32]byte
copy(key[:], []byte{byte(i)})
key := uint64(i)

entry := &SchemaCacheEntry{
RenderedInline: []byte{byte(i)},
Expand All @@ -97,8 +94,8 @@ func TestDefaultCache_Range(t *testing.T) {

// Range over all entries
count := 0
foundKeys := make(map[[32]byte]bool)
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
foundKeys := make(map[uint64]bool)
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
foundKeys[key] = true

Expand All @@ -118,14 +115,13 @@ func TestDefaultCache_RangeEarlyTermination(t *testing.T) {

// Store multiple entries
for i := 0; i < 10; i++ {
var key [32]byte
copy(key[:], []byte{byte(i)})
key := uint64(i)
cache.Store(key, &SchemaCacheEntry{})
}

// Range but stop after 3 iterations
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return count < 3 // Stop after 3
})
Expand All @@ -138,7 +134,7 @@ func TestDefaultCache_RangeNilCache(t *testing.T) {

// Should not panic
called := false
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
called = true
return true
})
Expand All @@ -151,7 +147,7 @@ func TestDefaultCache_RangeEmpty(t *testing.T) {

// Range over empty cache
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return true
})
Expand All @@ -162,8 +158,7 @@ func TestDefaultCache_RangeEmpty(t *testing.T) {
func TestDefaultCache_Overwrite(t *testing.T) {
cache := NewDefaultCache()

var key [32]byte
copy(key[:], []byte("test-key"))
key := uint64(0x12345678)

// Store first value
first := &SchemaCacheEntry{
Expand All @@ -188,10 +183,9 @@ func TestDefaultCache_MultipleKeys(t *testing.T) {
cache := NewDefaultCache()

// Store with different keys
var key1, key2, key3 [32]byte
copy(key1[:], []byte("key1"))
copy(key2[:], []byte("key2"))
copy(key3[:], []byte("key3"))
key1 := uint64(1)
key2 := uint64(2)
key3 := uint64(3)

cache.Store(key1, &SchemaCacheEntry{RenderedInline: []byte("value1")})
cache.Store(key2, &SchemaCacheEntry{RenderedInline: []byte("value2")})
Expand All @@ -218,8 +212,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(val int) {
var key [32]byte
copy(key[:], []byte{byte(val)})
key := uint64(val)
cache.Store(key, &SchemaCacheEntry{
RenderedInline: []byte{byte(val)},
})
Expand All @@ -235,8 +228,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {
// Concurrent reads
for i := 0; i < 10; i++ {
go func(val int) {
var key [32]byte
copy(key[:], []byte{byte(val)})
key := uint64(val)
loaded, ok := cache.Load(key)
assert.True(t, ok)
assert.NotNil(t, loaded)
Expand All @@ -251,7 +243,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {

// Verify all entries exist
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return true
})
Expand Down Expand Up @@ -284,20 +276,18 @@ func TestDefaultCache_RangeWithInvalidTypes(t *testing.T) {
cache.m.Store("invalid-key-type", &SchemaCacheEntry{})

// Store an entry with wrong value type
var validKey [32]byte
copy(validKey[:], []byte{1})
validKey := uint64(1)
cache.m.Store(validKey, "invalid-value-type")

// Store a valid entry
var validKey2 [32]byte
copy(validKey2[:], []byte{2})
validKey2 := uint64(2)
validEntry := &SchemaCacheEntry{RenderedInline: []byte("valid")}
cache.Store(validKey2, validEntry)

// Range should skip invalid entries and only process valid ones
count := 0
var seenEntry *SchemaCacheEntry
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
seenEntry = value
return true
Expand Down
8 changes: 4 additions & 4 deletions cache/default_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewDefaultCache() *DefaultCache {
}

// Load retrieves a schema from the cache.
func (c *DefaultCache) Load(key [32]byte) (*SchemaCacheEntry, bool) {
func (c *DefaultCache) Load(key uint64) (*SchemaCacheEntry, bool) {
if c == nil || c.m == nil {
return nil, false
}
Expand All @@ -28,20 +28,20 @@ func (c *DefaultCache) Load(key [32]byte) (*SchemaCacheEntry, bool) {
}

// Store saves a schema to the cache.
func (c *DefaultCache) Store(key [32]byte, value *SchemaCacheEntry) {
func (c *DefaultCache) Store(key uint64, value *SchemaCacheEntry) {
if c == nil || c.m == nil {
return
}
c.m.Store(key, value)
}

// Range calls f for each entry in the cache (for testing/inspection).
func (c *DefaultCache) Range(f func(key [32]byte, value *SchemaCacheEntry) bool) {
func (c *DefaultCache) Range(f func(key uint64, value *SchemaCacheEntry) bool) {
if c == nil || c.m == nil {
return
}
c.m.Range(func(k, v interface{}) bool {
key, ok := k.([32]byte)
key, ok := k.(uint64)
if !ok {
return true
}
Expand Down
Loading