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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.7
require (
github.com/dlclark/regexp2 v1.11.5
github.com/pb33f/jsonpath v0.1.2
github.com/pb33f/libopenapi v0.27.0
github.com/pb33f/libopenapi v0.27.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.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ 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/libopenapi v0.27.0 h1:MVxnb9AWDcT2o03rEv/qEmFww+cgS/NwZtkGk52Mhws=
github.com/pb33f/libopenapi v0.27.0/go.mod h1:mHMHA3ZKSZDTInNAuUtqkHlKLIjPm2HN1vgsGR57afc=
github.com/pb33f/libopenapi v0.27.2 h1:buDwqA6SKWwrG4YZwJprBV+bk8uizqLgiF4lkIV9w14=
github.com/pb33f/libopenapi v0.27.2/go.mod h1:mHMHA3ZKSZDTInNAuUtqkHlKLIjPm2HN1vgsGR57afc=
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
9 changes: 3 additions & 6 deletions helpers/schema_compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ func NewCompiledSchema(name string, jsonSchema []byte, o *config.ValidationOptio
// - version 3.0: Allows OpenAPI 3.0 keywords like 'nullable'
// - version 3.1+: Rejects OpenAPI 3.0 keywords like 'nullable' (strict JSON Schema compliance)
func NewCompiledSchemaWithVersion(name string, jsonSchema []byte, o *config.ValidationOptions, version float32) (*jsonschema.Schema, error) {
// fake-Up a resource name for the schema
resourceName := fmt.Sprintf("%s.json", name)

compiler := NewCompilerWithOptions(o)
compiler.UseLoader(NewCompilerLoader())

Expand Down Expand Up @@ -88,13 +85,13 @@ func NewCompiledSchemaWithVersion(name string, jsonSchema []byte, o *config.Vali
return nil, fmt.Errorf("failed to unmarshal JSON schema: %w", err)
}

if err = compiler.AddResource(resourceName, decodedSchema); err != nil {
if err = compiler.AddResource(name, decodedSchema); err != nil {
return nil, fmt.Errorf("failed to add resource to schema compiler: %w", err)
}

jsch, err := compiler.Compile(resourceName)
jsch, err := compiler.Compile(name)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema: %w", err)
return nil, fmt.Errorf("JSON schema compile failed: %s", err.Error())
}

return jsch, nil
Expand Down
2 changes: 1 addition & 1 deletion responses/validate_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1364,7 +1364,7 @@ components:

assert.False(t, valid)
assert.Len(t, errors, 1)
assert.Equal(t, "cannot render circular reference: #/components/schemas/Error", errors[0].Reason)
assert.Equal(t, "schema render failure, circular reference: `#/components/schemas/Error`", errors[0].Reason)
}

func TestValidateBody_CheckHeader(t *testing.T) {
Expand Down
54 changes: 45 additions & 9 deletions schema_validation/validate_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,31 @@ 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.
s.lock.Lock()
renderedSchema, _ = schema.RenderInline()
var e error
renderedSchema, e = schema.RenderInline()
if e != nil {
// schema cannot be rendered, so it's not valid!
violation := &liberrors.SchemaValidationFailure{
Reason: e.Error(),
Location: "unavailable",
ReferenceSchema: string(renderedSchema),
ReferenceObject: string(payload),
}
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: "schema does not pass validation",
Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()),
SpecLine: schema.GoLow().GetRootNode().Line,
SpecCol: schema.GoLow().GetRootNode().Column,
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
HowToFix: liberrors.HowToFixInvalidSchema,
Context: string(renderedSchema),
})
s.lock.Unlock()
return false, validationErrors

}
s.lock.Unlock()

jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema)
Expand All @@ -145,23 +169,29 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
ReferenceSchema: string(renderedSchema),
ReferenceObject: string(payload),
}
line := 1
col := 0
if schema.GoLow().Type.KeyNode != nil {
line = schema.GoLow().Type.KeyNode.Line
col = schema.GoLow().Type.KeyNode.Column
}
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.RequestBodyValidation,
ValidationSubType: helpers.Schema,
Message: "schema does not pass validation",
Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()),
SpecLine: 1,
SpecCol: 0,
SpecLine: line,
SpecCol: col,
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
HowToFix: liberrors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
Context: string(renderedSchema),
})
return false, validationErrors
}

}

jsch, err := helpers.NewCompiledSchemaWithVersion("schema", jsonSchema, s.options, version)
jsch, err := helpers.NewCompiledSchemaWithVersion(schema.GoLow().GetIndex().GetSpecAbsolutePath(), jsonSchema, s.options, version)

var schemaValidationErrors []*liberrors.SchemaValidationFailure
if err != nil {
Expand All @@ -171,16 +201,22 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
ReferenceSchema: string(renderedSchema),
ReferenceObject: string(payload),
}
line := 1
col := 0
if schema.GoLow().Type.KeyNode != nil {
line = schema.GoLow().Type.KeyNode.Line
col = schema.GoLow().Type.KeyNode.Column
}
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: helpers.Schema,
ValidationSubType: helpers.Schema,
Message: "schema compilation failed",
Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()),
SpecLine: 1,
SpecCol: 0,
SpecLine: line,
SpecCol: col,
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
HowToFix: liberrors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
Context: string(renderedSchema),
})
return false, validationErrors
}
Expand Down Expand Up @@ -212,7 +248,7 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
SpecCol: col,
SchemaValidationErrors: schemaValidationErrors,
HowToFix: liberrors.HowToFixInvalidSchema,
Context: string(renderedSchema), // attach the rendered schema to the error
Context: string(renderedSchema),
})
}
}
Expand Down
142 changes: 142 additions & 0 deletions schema_validation/validate_schema_openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,148 @@ paths:
assert.Empty(t, errors, "Should have no validation errors")
}

func TestValidateSchema_CircularReference(t *testing.T) {
spec := `openapi: "3.1.0"
info:
title: Test
version: "1"
paths:
/:
post:
operationId: op
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/c'
components:
schemas:
a:
type: "string"
examples:
- ''
b:
type: "object"
examples:
- { "z": "" }
properties:
z:
"$ref": '#/components/schemas/a'
b:
"$ref": '#/components/schemas/b'
c:
type: "object"
examples:
- { "b": { "z": "" } }
properties:
"b":
"$ref": '#/components/schemas/b'`

doc, err := libopenapi.NewDocument([]byte(spec))
assert.NoError(t, err)

model, errs := doc.BuildV3Model()
assert.Nil(t, errs)

schema := model.Model.Paths.PathItems.GetOrZero("/").Post.RequestBody.Content.GetOrZero("application/json").Schema
assert.NotNil(t, schema)
assert.NotNil(t, schema.Schema())

t.Run("should fail rendering", func(t *testing.T) {
_, err := schema.Schema().RenderInline()
assert.Error(t, err, "RenderInline should not error on circular refs")
})

t.Run("should fail validating", func(t *testing.T) {
sv := NewSchemaValidator()

schemaB := model.Model.Components.Schemas.GetOrZero("b").Schema()

assert.NotNil(t, schemaB)
assert.NotNil(t, schemaB.Examples)

exampleJSON := `{"z": "", "b": {"z": ""}}`
valid, errors := sv.ValidateSchemaString(schemaB, exampleJSON)

assert.False(t, valid, "Schema with circular refs should currently fail validation")
assert.NotNil(t, errors, "Should have validation errors")

foundCompilationError := false
for _, err := range errors {
if err.SchemaValidationErrors != nil {
for _, schErr := range err.SchemaValidationErrors {
if schErr.Location == "unavailable" && schErr.Reason == "schema render failure, circular reference: `#/components/schemas/b`" {
foundCompilationError = true
}
}
}
}
assert.True(t, foundCompilationError, "Should have schema compilation error for circular references")
})
}

func TestValidateSchema_SimpleCircularReference(t *testing.T) {
// Even simpler test case
spec := `openapi: "3.1.0"
info:
title: Test
version: "1"
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Node'
components:
schemas:
Node:
type: object
properties:
value:
type: string
next:
$ref: '#/components/schemas/Node'
examples:
- value: "test"
next:
value: "nested"`

doc, err := libopenapi.NewDocument([]byte(spec))
assert.NoError(t, err)

model, errs := doc.BuildV3Model()
assert.Nil(t, errs)

schema := model.Model.Paths.PathItems.GetOrZero("/test").Get.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/json").Schema
assert.NotNil(t, schema)
assert.NotNil(t, schema.Schema())

// Try to render inline
rendered, err := schema.Schema().RenderInline()
if err != nil {
t.Logf("RenderInline error on simple circular ref: %v", err)
} else {
t.Logf("RenderInline succeeded for simple circular ref, rendered %d bytes", len(rendered))
}

// Validate using schema validator
sv := NewSchemaValidator()
nodeSchema := model.Model.Components.Schemas.GetOrZero("Node").Schema()

// Try to validate an example against the schema
exampleJSON := `{"value": "test", "next": {"value": "nested"}}`
valid, errors := sv.ValidateSchemaString(nodeSchema, exampleJSON)

t.Logf("Simple circular ref - Schema validation valid: %v", valid)
for _, err := range errors {
t.Logf("Error: %s", err.Error())
}
}

// Helper function to check if a string contains a substring (case-insensitive)
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
Expand Down
2 changes: 1 addition & 1 deletion validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,7 @@ components:
}
if ok, errs := oapiValidator.ValidateHttpResponse(req, res); !ok {
assert.Equal(t, 1, len(errs))
assert.Equal(t, "cannot render circular reference: #/components/schemas/Error", errs[0].Reason)
assert.Equal(t, "schema render failure, circular reference: `#/components/schemas/Error`", errs[0].Reason)

}
}
Expand Down