From c8bd150ad5164a8475163fbae6c1bbee079b40f1 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Thu, 18 Dec 2025 17:27:01 -0500 Subject: [PATCH 1/3] bumps deps and uses new context aware rendering. --- go.mod | 12 ++++++------ go.sum | 10 ++++++++++ parameters/validate_parameter.go | 6 ++++-- requests/validate_request.go | 3 ++- responses/validate_response.go | 3 ++- schema_validation/validate_schema.go | 5 ++++- validator.go | 6 ++++-- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index b74bd11..ae6d5a4 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 ) diff --git a/go.sum b/go.sum index b77eded..433315f 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index bdd8034..f31ffd1 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -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. @@ -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) } diff --git a/requests/validate_request.go b/requests/validate_request.go index 03cc20e..04c98e6 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -75,7 +75,8 @@ 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() + renderedSchema, _ = input.Schema.RenderInlineWithContext(renderCtx) referenceSchema = string(renderedSchema) jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) diff --git a/responses/validate_response.go b/responses/validate_response.go index ab6c4af..dddf8cd 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -78,7 +78,8 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors // Cache miss or no cache - render and compile if compiledSchema == nil { - renderedSchema, _ = input.Schema.RenderInline() + renderCtx := base.NewInlineRenderContext() + renderedSchema, _ = input.Schema.RenderInlineWithContext(renderCtx) referenceSchema = string(renderedSchema) jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 9f2b139..0fded10 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -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{ diff --git a/validator.go b/validator.go index 4d9660c..79ebdb3 100644 --- a/validator.go +++ b/validator.go @@ -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 { @@ -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 { From 41d8a04fc076fb025415b1864e8caa909f89a9cd Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Thu, 18 Dec 2025 17:29:07 -0500 Subject: [PATCH 2/3] update workflow --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e65be1d..4c1cdc8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,7 @@ jobs: - 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 From 4cd3a3f20b859584f6a3666e098791a8e6a0c5e7 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Thu, 18 Dec 2025 18:01:03 -0500 Subject: [PATCH 3/3] fix failing test with missed error handling. Also bumped the version of the golangci-lint tool --- .github/workflows/build.yaml | 2 +- requests/validate_request.go | 27 ++++++++++++++++++- requests/validate_request_test.go | 41 ++++++++++++++++++++++++++++ responses/validate_response.go | 27 ++++++++++++++++++- responses/validate_response_test.go | 42 +++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4c1cdc8..f9dbb25 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,7 +24,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: v2.1 + version: v2.7 build: name: Build runs-on: ubuntu-latest diff --git a/requests/validate_request.go b/requests/validate_request.go index 04c98e6..2d5aeca 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -76,8 +76,33 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*errors.V // Cache miss or no cache - render and compile if compiledSchema == nil { renderCtx := base.NewInlineRenderContext() - renderedSchema, _ = input.Schema.RenderInlineWithContext(renderCtx) + 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 diff --git a/requests/validate_request_test.go b/requests/validate_request_test.go index de66448..c47f120 100644 --- a/requests/validate_request_test.go +++ b/requests/validate_request_test.go @@ -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") +} diff --git a/responses/validate_response.go b/responses/validate_response.go index dddf8cd..edeaaee 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -79,8 +79,33 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*errors // Cache miss or no cache - render and compile if compiledSchema == nil { renderCtx := base.NewInlineRenderContext() - renderedSchema, _ = input.Schema.RenderInlineWithContext(renderCtx) + 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 diff --git a/responses/validate_response_test.go b/responses/validate_response_test.go index 7fe5e45..241e1ac 100644 --- a/responses/validate_response_test.go +++ b/responses/validate_response_test.go @@ -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") +}