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
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
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
module github.com/pb33f/libopenapi-validator

go 1.24.7
go 1.25.0

require (
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad
github.com/dlclark/regexp2 v1.11.5
github.com/goccy/go-yaml v1.18.0
github.com/pb33f/jsonpath v0.1.2
github.com/pb33f/libopenapi v0.28.2
github.com/goccy/go-yaml v1.19.1
github.com/pb33f/jsonpath v0.7.0
github.com/pb33f/libopenapi v0.30.2
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/text v0.31.0
golang.org/x/text v0.32.0
)

require (
Expand All @@ -20,6 +20,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pb33f/ordered-map/v2 v2.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pb33f/jsonpath v0.1.2 h1:PlqXjEyecMqoYJupLxYeClCGWEpAFnh4pmzgspbXDPI=
github.com/pb33f/jsonpath v0.1.2/go.mod h1:TtKnUnfqZm48q7a56DxB3WtL3ipkVtukMKGKxaR/uXU=
github.com/pb33f/jsonpath v0.7.0 h1:3oG6yu1RqNoMZpqnRjBMqi8fSIXWoDAKDrsB0QGTcoU=
github.com/pb33f/jsonpath v0.7.0/go.mod h1:/+JlSIjWA2ijMVYGJ3IQPF4Q1nLMYbUTYNdk0exCDPQ=
github.com/pb33f/libopenapi v0.28.2 h1:AXVCE8DWzytXu0jv0Z+cXVopnO/bXU1oWvgA9qiRWgw=
github.com/pb33f/libopenapi v0.28.2/go.mod h1:mHMHA3ZKSZDTInNAuUtqkHlKLIjPm2HN1vgsGR57afc=
github.com/pb33f/libopenapi v0.30.2 h1:xOldKP2h5rnBs3Q1EsJULgcplGz2iEem7FybLX8TySU=
github.com/pb33f/libopenapi v0.30.2/go.mod h1:4MP76dnaTMY+DM+bRhKBneAIhVISEEZM6G6sd7A9pus=
github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ=
github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -49,6 +55,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -73,6 +81,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
6 changes: 4 additions & 2 deletions parameters/validate_parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ func ValidateParameterSchema(
var validationErrors []*errors.ValidationError

// 1. build a JSON render of the schema.
renderedSchema, _ := schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
renderedSchema, _ := schema.RenderInlineWithContext(renderCtx)
jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema)

// 2. decode the object into a json blob.
Expand Down Expand Up @@ -234,7 +235,8 @@ func formatJsonSchemaValidationError(schema *base.Schema, scErrs *jsonschema.Val
OriginalError: scErrs,
}
if schema != nil {
rendered, err := schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
rendered, err := schema.RenderInlineWithContext(renderCtx)
if err == nil && rendered != nil {
fail.ReferenceSchema = string(rendered)
}
Expand Down
28 changes: 27 additions & 1 deletion requests/validate_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,34 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V

// Cache miss or no cache - render and compile
if compiledSchema == nil {
renderedSchema, _ = input.Schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
var renderErr error
renderedSchema, renderErr = input.Schema.RenderInlineWithContext(renderCtx)
referenceSchema = string(renderedSchema)

// If rendering failed (e.g., circular reference), return the render error
if renderErr != nil {
violation := &errors.SchemaValidationFailure{
Reason: renderErr.Error(),
Location: "schema rendering",
ReferenceSchema: referenceSchema,
}
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: fmt.Sprintf("%s request body for '%s' failed schema rendering",
input.Request.Method, input.Request.URL.Path),
Reason: fmt.Sprintf("The request schema failed to render: %s",
renderErr.Error()),
SpecLine: 1,
SpecCol: 0,
SchemaValidationErrors: []*errors.SchemaValidationFailure{violation},
HowToFix: "check the request schema for circular references or invalid structures",
Context: referenceSchema,
})
return false, validationErrors
}

jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema)

var err error
Expand Down
41 changes: 41 additions & 0 deletions requests/validate_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,44 @@ func indentLines(s string, indent string) string {
}
return strings.Join(lines, "\n")
}

func TestValidateRequestSchema_CircularReference(t *testing.T) {
// Test when schema has a circular reference that causes render failure
spec := `openapi: 3.1.0
info:
title: Test
version: 1.0.0
components:
schemas:
Error:
type: object
properties:
code:
type: string
details:
type: array
items:
$ref: '#/components/schemas/Error'`

doc, err := libopenapi.NewDocument([]byte(spec))
require.NoError(t, err)
model, errs := doc.BuildV3Model()
require.Empty(t, errs)

// Verify circular reference was detected
require.Len(t, model.Index.GetCircularReferences(), 1)

schema := model.Model.Components.Schemas.GetOrZero("Error")
require.NotNil(t, schema)

valid, errors := ValidateRequestSchema(&ValidateRequestSchemaInput{
Request: postRequestWithBody(`{"code": "abc", "details": [{"code": "def"}]}`),
Schema: schema.Schema(),
Version: 3.1,
})

assert.False(t, valid)
require.Len(t, errors, 1)
assert.Contains(t, errors[0].Message, "failed schema rendering")
assert.Contains(t, errors[0].Reason, "circular reference")
}
28 changes: 27 additions & 1 deletion responses/validate_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,34 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors

// Cache miss or no cache - render and compile
if compiledSchema == nil {
renderedSchema, _ = input.Schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
var renderErr error
renderedSchema, renderErr = input.Schema.RenderInlineWithContext(renderCtx)
referenceSchema = string(renderedSchema)

// If rendering failed (e.g., circular reference), return the render error
if renderErr != nil {
violation := &errors.SchemaValidationFailure{
Reason: renderErr.Error(),
Location: "schema rendering",
ReferenceSchema: referenceSchema,
}
validationErrors = append(validationErrors, &errors.ValidationError{
ValidationType: helpers.ResponseBodyValidation,
ValidationSubType: helpers.Schema,
Message: fmt.Sprintf("%d response body for '%s' failed schema rendering",
input.Response.StatusCode, input.Request.URL.Path),
Reason: fmt.Sprintf("The response schema for status code '%d' failed to render: %s",
input.Response.StatusCode, renderErr.Error()),
SpecLine: 1,
SpecCol: 0,
SchemaValidationErrors: []*errors.SchemaValidationFailure{violation},
HowToFix: "check the response schema for circular references or invalid structures",
Context: referenceSchema,
})
return false, validationErrors
}

jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema)

var err error
Expand Down
42 changes: 42 additions & 0 deletions responses/validate_response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,45 @@ func TestValidateResponseSchema_NilSchemaGoLow(t *testing.T) {
assert.Equal(t, "schema cannot be rendered", errors[0].Message)
assert.Contains(t, errors[0].Reason, "does not have low-level information")
}

func TestValidateResponseSchema_CircularReference(t *testing.T) {
// Test when schema has a circular reference that causes render failure
spec := `openapi: 3.1.0
info:
title: Test
version: 1.0.0
components:
schemas:
Error:
type: object
properties:
code:
type: string
details:
type: array
items:
$ref: '#/components/schemas/Error'`

doc, err := libopenapi.NewDocument([]byte(spec))
require.NoError(t, err)
model, errs := doc.BuildV3Model()
require.Empty(t, errs)

// Verify circular reference was detected
require.Len(t, model.Index.GetCircularReferences(), 1)

schema := model.Model.Components.Schemas.GetOrZero("Error")
require.NotNil(t, schema)

valid, errors := ValidateResponseSchema(&ValidateResponseSchemaInput{
Request: postRequest(),
Response: responseWithBody(`{"code": "abc", "details": [{"code": "def"}]}`),
Schema: schema.Schema(),
Version: 3.1,
})

assert.False(t, valid)
require.Len(t, errors, 1)
assert.Contains(t, errors[0].Message, "failed schema rendering")
assert.Contains(t, errors[0].Reason, "circular reference")
}
5 changes: 4 additions & 1 deletion schema_validation/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,12 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload

// render the schema, to be used for validation, stop this from running concurrently, mutations are made to state
// and, it will cause async issues.
// Create isolated render context for this validation to prevent false positive cycle detection
// when multiple validations run concurrently.
renderCtx := base.NewInlineRenderContext()
s.lock.Lock()
var e error
renderedSchema, e = schema.RenderInline()
renderedSchema, e = schema.RenderInlineWithContext(renderCtx)
if e != nil {
// schema cannot be rendered, so it's not valid!
violation := &liberrors.SchemaValidationFailure{
Expand Down
6 changes: 4 additions & 2 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ func warmMediaTypeSchema(mediaType *v3.MediaType, schemaCache cache.SchemaCache,
if _, exists := schemaCache.Load(hash); !exists {
schema := mediaType.Schema.Schema()
if schema != nil {
renderedInline, _ := schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
renderedInline, _ := schema.RenderInlineWithContext(renderCtx)
referenceSchema := string(renderedInline)
renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline)
if len(renderedInline) > 0 {
Expand Down Expand Up @@ -515,7 +516,8 @@ func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, opt

if schema != nil {
if _, exists := schemaCache.Load(hash); !exists {
renderedInline, _ := schema.RenderInline()
renderCtx := base.NewInlineRenderContext()
renderedInline, _ := schema.RenderInlineWithContext(renderCtx)
referenceSchema := string(renderedInline)
renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline)
if len(renderedInline) > 0 {
Expand Down