Skip to content

Commit 8b586d4

Browse files
committed
add proper schema validation handling.
1 parent 3eb1cae commit 8b586d4

File tree

5 files changed

+194
-14
lines changed

5 files changed

+194
-14
lines changed

helpers/schema_compiler.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ func NewCompiledSchema(name string, jsonSchema []byte, o *config.ValidationOptio
5555
// - version 3.0: Allows OpenAPI 3.0 keywords like 'nullable'
5656
// - version 3.1+: Rejects OpenAPI 3.0 keywords like 'nullable' (strict JSON Schema compliance)
5757
func NewCompiledSchemaWithVersion(name string, jsonSchema []byte, o *config.ValidationOptions, version float32) (*jsonschema.Schema, error) {
58-
// fake-Up a resource name for the schema
59-
resourceName := fmt.Sprintf("%s.json", name)
58+
resourceName := fmt.Sprintf("%s", name)
6059

6160
compiler := NewCompilerWithOptions(o)
6261
compiler.UseLoader(NewCompilerLoader())
@@ -94,7 +93,7 @@ func NewCompiledSchemaWithVersion(name string, jsonSchema []byte, o *config.Vali
9493

9594
jsch, err := compiler.Compile(resourceName)
9695
if err != nil {
97-
return nil, fmt.Errorf("failed to compile JSON schema: %w", err)
96+
return nil, fmt.Errorf("JSON schema compile failed: %s", err.Error())
9897
}
9998

10099
return jsch, nil

responses/validate_body_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,7 @@ components:
13641364

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

13701370
func TestValidateBody_CheckHeader(t *testing.T) {

schema_validation/validate_schema.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,31 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
129129
// render the schema, to be used for validation, stop this from running concurrently, mutations are made to state
130130
// and, it will cause async issues.
131131
s.lock.Lock()
132-
renderedSchema, _ = schema.RenderInline()
132+
var e error
133+
renderedSchema, e = schema.RenderInline()
134+
if e != nil {
135+
// schema cannot be rendered, so it's not valid!
136+
violation := &liberrors.SchemaValidationFailure{
137+
Reason: e.Error(),
138+
Location: "unavailable",
139+
ReferenceSchema: string(renderedSchema),
140+
ReferenceObject: string(payload),
141+
}
142+
validationErrors = append(validationErrors, &liberrors.ValidationError{
143+
ValidationType: helpers.RequestBodyValidation,
144+
ValidationSubType: helpers.Schema,
145+
Message: "schema does not pass validation",
146+
Reason: fmt.Sprintf("The schema cannot be decoded: %s", e.Error()),
147+
SpecLine: schema.GoLow().GetRootNode().Line,
148+
SpecCol: schema.GoLow().GetRootNode().Column,
149+
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
150+
HowToFix: liberrors.HowToFixInvalidSchema,
151+
Context: string(renderedSchema),
152+
})
153+
s.lock.Unlock()
154+
return false, validationErrors
155+
156+
}
133157
s.lock.Unlock()
134158

135159
jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema)
@@ -145,23 +169,29 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
145169
ReferenceSchema: string(renderedSchema),
146170
ReferenceObject: string(payload),
147171
}
172+
line := 1
173+
col := 0
174+
if schema.GoLow().Type.KeyNode != nil {
175+
line = schema.GoLow().Type.KeyNode.Line
176+
col = schema.GoLow().Type.KeyNode.Column
177+
}
148178
validationErrors = append(validationErrors, &liberrors.ValidationError{
149179
ValidationType: helpers.RequestBodyValidation,
150180
ValidationSubType: helpers.Schema,
151181
Message: "schema does not pass validation",
152182
Reason: fmt.Sprintf("The schema cannot be decoded: %s", err.Error()),
153-
SpecLine: 1,
154-
SpecCol: 0,
183+
SpecLine: line,
184+
SpecCol: col,
155185
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
156186
HowToFix: liberrors.HowToFixInvalidSchema,
157-
Context: string(renderedSchema), // attach the rendered schema to the error
187+
Context: string(renderedSchema),
158188
})
159189
return false, validationErrors
160190
}
161191

162192
}
163193

164-
jsch, err := helpers.NewCompiledSchemaWithVersion("schema", jsonSchema, s.options, version)
194+
jsch, err := helpers.NewCompiledSchemaWithVersion(schema.GoLow().GetIndex().GetSpecAbsolutePath(), jsonSchema, s.options, version)
165195

166196
var schemaValidationErrors []*liberrors.SchemaValidationFailure
167197
if err != nil {
@@ -171,16 +201,22 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
171201
ReferenceSchema: string(renderedSchema),
172202
ReferenceObject: string(payload),
173203
}
204+
line := 1
205+
col := 0
206+
if schema.GoLow().Type.KeyNode != nil {
207+
line = schema.GoLow().Type.KeyNode.Line
208+
col = schema.GoLow().Type.KeyNode.Column
209+
}
174210
validationErrors = append(validationErrors, &liberrors.ValidationError{
175211
ValidationType: helpers.Schema,
176212
ValidationSubType: helpers.Schema,
177213
Message: "schema compilation failed",
178214
Reason: fmt.Sprintf("Schema compilation failed: %s", err.Error()),
179-
SpecLine: 1,
180-
SpecCol: 0,
215+
SpecLine: line,
216+
SpecCol: col,
181217
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
182218
HowToFix: liberrors.HowToFixInvalidSchema,
183-
Context: string(renderedSchema), // attach the rendered schema to the error
219+
Context: string(renderedSchema),
184220
})
185221
return false, validationErrors
186222
}
@@ -212,7 +248,7 @@ func (s *schemaValidator) validateSchemaWithVersion(schema *base.Schema, payload
212248
SpecCol: col,
213249
SchemaValidationErrors: schemaValidationErrors,
214250
HowToFix: liberrors.HowToFixInvalidSchema,
215-
Context: string(renderedSchema), // attach the rendered schema to the error
251+
Context: string(renderedSchema),
216252
})
217253
}
218254
}

schema_validation/validate_schema_openapi_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,151 @@ paths:
277277
assert.Empty(t, errors, "Should have no validation errors")
278278
}
279279

280+
func TestValidateSchema_CircularReference(t *testing.T) {
281+
spec := `openapi: "3.1.0"
282+
info:
283+
title: Test
284+
version: "1"
285+
paths:
286+
/:
287+
post:
288+
operationId: op
289+
requestBody:
290+
content:
291+
application/json:
292+
schema:
293+
$ref: '#/components/schemas/c'
294+
components:
295+
schemas:
296+
a:
297+
type: "string"
298+
examples:
299+
- ''
300+
b:
301+
type: "object"
302+
examples:
303+
- { "z": "" }
304+
properties:
305+
z:
306+
"$ref": '#/components/schemas/a'
307+
b:
308+
"$ref": '#/components/schemas/b'
309+
c:
310+
type: "object"
311+
examples:
312+
- { "b": { "z": "" } }
313+
properties:
314+
"b":
315+
"$ref": '#/components/schemas/b'`
316+
317+
doc, err := libopenapi.NewDocument([]byte(spec))
318+
assert.NoError(t, err)
319+
320+
model, errs := doc.BuildV3Model()
321+
assert.Nil(t, errs)
322+
323+
schema := model.Model.Paths.PathItems.GetOrZero("/").Post.RequestBody.Content.GetOrZero("application/json").Schema
324+
assert.NotNil(t, schema)
325+
assert.NotNil(t, schema.Schema())
326+
327+
t.Run("should fail rendering", func(t *testing.T) {
328+
_, err := schema.Schema().RenderInline()
329+
assert.Error(t, err, "RenderInline should not error on circular refs")
330+
331+
})
332+
333+
t.Run("should fail validating", func(t *testing.T) {
334+
sv := NewSchemaValidator()
335+
336+
schemaB := model.Model.Components.Schemas.GetOrZero("b").Schema()
337+
338+
assert.NotNil(t, schemaB)
339+
assert.NotNil(t, schemaB.Examples)
340+
341+
exampleJSON := `{"z": "", "b": {"z": ""}}`
342+
valid, errors := sv.ValidateSchemaString(schemaB, exampleJSON)
343+
344+
assert.False(t, valid, "Schema with circular refs should currently fail validation")
345+
assert.NotNil(t, errors, "Should have validation errors")
346+
347+
foundCompilationError := false
348+
for _, err := range errors {
349+
if err.SchemaValidationErrors != nil {
350+
for _, schErr := range err.SchemaValidationErrors {
351+
if schErr.Location == "unavailable" && schErr.Reason == "schema render failure, circular reference: `#/components/schemas/b`" {
352+
foundCompilationError = true
353+
}
354+
}
355+
}
356+
}
357+
assert.True(t, foundCompilationError, "Should have schema compilation error for circular references")
358+
})
359+
}
360+
361+
func TestValidateSchema_SimpleCircularReference(t *testing.T) {
362+
// Even simpler test case
363+
spec := `openapi: "3.1.0"
364+
info:
365+
title: Test
366+
version: "1"
367+
paths:
368+
/test:
369+
get:
370+
responses:
371+
'200':
372+
description: OK
373+
content:
374+
application/json:
375+
schema:
376+
$ref: '#/components/schemas/Node'
377+
components:
378+
schemas:
379+
Node:
380+
type: object
381+
properties:
382+
value:
383+
type: string
384+
next:
385+
$ref: '#/components/schemas/Node'
386+
examples:
387+
- value: "test"
388+
next:
389+
value: "nested"`
390+
391+
doc, err := libopenapi.NewDocument([]byte(spec))
392+
assert.NoError(t, err)
393+
394+
model, errs := doc.BuildV3Model()
395+
assert.Nil(t, errs)
396+
397+
schema := model.Model.Paths.PathItems.GetOrZero("/test").Get.Responses.Codes.GetOrZero("200").Content.GetOrZero("application/json").Schema
398+
assert.NotNil(t, schema)
399+
assert.NotNil(t, schema.Schema())
400+
401+
// Try to render inline
402+
rendered, err := schema.Schema().RenderInline()
403+
if err != nil {
404+
t.Logf("RenderInline error on simple circular ref: %v", err)
405+
} else {
406+
t.Logf("RenderInline succeeded for simple circular ref, rendered %d bytes", len(rendered))
407+
}
408+
409+
// Validate using schema validator
410+
sv := NewSchemaValidator()
411+
nodeSchema := model.Model.Components.Schemas.GetOrZero("Node").Schema()
412+
413+
// Try to validate an example against the schema
414+
exampleJSON := `{"value": "test", "next": {"value": "nested"}}`
415+
valid, errors := sv.ValidateSchemaString(nodeSchema, exampleJSON)
416+
417+
t.Logf("Simple circular ref - Schema validation valid: %v", valid)
418+
if errors != nil {
419+
for _, err := range errors {
420+
t.Logf("Error: %s", err.Error())
421+
}
422+
}
423+
}
424+
280425
// Helper function to check if a string contains a substring (case-insensitive)
281426
func contains(s, substr string) bool {
282427
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||

validator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1829,7 +1829,7 @@ components:
18291829
}
18301830
if ok, errs := oapiValidator.ValidateHttpResponse(req, res); !ok {
18311831
assert.Equal(t, 1, len(errs))
1832-
assert.Equal(t, "cannot render circular reference: #/components/schemas/Error", errs[0].Reason)
1832+
assert.Equal(t, "schema render failure, circular reference: `#/components/schemas/Error`", errs[0].Reason)
18331833

18341834
}
18351835
}

0 commit comments

Comments
 (0)