Skip to content

Commit bec5cfe

Browse files
committed
Add ability to pass in pre-compiled schemas
1 parent 5c03f9d commit bec5cfe

2 files changed

Lines changed: 124 additions & 20 deletions

File tree

schema_validation/validate_document.go

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package schema_validation
55

66
import (
7+
"bytes"
78
"encoding/json"
89
"errors"
910
"fmt"
@@ -29,14 +30,22 @@ func normalizeJSON(data any) any {
2930
// ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version)
3031
// It will return true if the document is valid, false if it is not and a slice of ValidationError pointers.
3132
func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bool, []*liberrors.ValidationError) {
33+
return ValidateOpenAPIDocumentWithPrecompiled(doc, nil, opts...)
34+
}
35+
36+
// ValidateOpenAPIDocumentWithPrecompiled validates an OpenAPI document against the OAS JSON Schema.
37+
// When compiledSchema is non-nil it is used directly, skipping schema compilation.
38+
// When SpecJSONBytes is available on the document's SpecInfo, the normalizeJSON round-trip is
39+
// bypassed in favour of a single jsonschema.UnmarshalJSON call.
40+
func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSchema *jsonschema.Schema, opts ...config.Option) (bool, []*liberrors.ValidationError) {
3241
options := config.NewValidationOptions(opts...)
3342

3443
info := doc.GetSpecInfo()
3544
loadedSchema := info.APISchema
3645
var validationErrors []*liberrors.ValidationError
3746

38-
// Check if SpecJSON is nil before dereferencing
39-
if info.SpecJSON == nil {
47+
// Check if both JSON representations are nil before proceeding
48+
if info.SpecJSON == nil && info.SpecJSONBytes == nil {
4049
validationErrors = append(validationErrors, &liberrors.ValidationError{
4150
ValidationType: helpers.Schema,
4251
ValidationSubType: "document",
@@ -50,27 +59,44 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo
5059
return false, validationErrors
5160
}
5261

53-
decodedDocument := *info.SpecJSON
62+
// Use the precompiled schema if provided, otherwise compile it
63+
jsch := compiledSchema
64+
if jsch == nil {
65+
var err error
66+
jsch, err = helpers.NewCompiledSchema("schema", []byte(loadedSchema), options)
67+
if err != nil {
68+
validationErrors = append(validationErrors, &liberrors.ValidationError{
69+
ValidationType: helpers.Schema,
70+
ValidationSubType: "compilation",
71+
Message: "OpenAPI document schema compilation failed",
72+
Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()),
73+
SpecLine: 1,
74+
SpecCol: 0,
75+
HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs",
76+
Context: loadedSchema,
77+
})
78+
return false, validationErrors
79+
}
80+
}
5481

55-
// Compile the JSON Schema
56-
jsch, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options)
57-
if err != nil {
58-
// schema compilation failed, return validation error instead of panicking
59-
validationErrors = append(validationErrors, &liberrors.ValidationError{
60-
ValidationType: helpers.Schema,
61-
ValidationSubType: "compilation",
62-
Message: "OpenAPI document schema compilation failed",
63-
Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()),
64-
SpecLine: 1,
65-
SpecCol: 0,
66-
HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs",
67-
Context: loadedSchema,
68-
})
69-
return false, validationErrors
82+
// Build the normalized document value for validation.
83+
// Prefer SpecJSONBytes (single unmarshal) over SpecJSON (marshal+unmarshal round-trip).
84+
var normalized any
85+
if info.SpecJSONBytes != nil && len(*info.SpecJSONBytes) > 0 {
86+
var err error
87+
normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*info.SpecJSONBytes))
88+
if err != nil {
89+
// Fall back to normalizeJSON if UnmarshalJSON fails
90+
if info.SpecJSON != nil {
91+
normalized = normalizeJSON(*info.SpecJSON)
92+
}
93+
}
94+
} else if info.SpecJSON != nil {
95+
normalized = normalizeJSON(*info.SpecJSON)
7096
}
7197

7298
// Validate the document
73-
scErrs := jsch.Validate(normalizeJSON(decodedDocument))
99+
scErrs := jsch.Validate(normalized)
74100

75101
var schemaValidationErrors []*liberrors.SchemaValidationFailure
76102

schema_validation/validate_document_test.go

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,10 @@ info:
177177

178178
doc, _ := libopenapi.NewDocument([]byte(spec))
179179

180-
// Simulate the nil SpecJSON scenario by setting it to nil
180+
// Simulate the nil SpecJSON scenario by setting both to nil
181181
info := doc.GetSpecInfo()
182182
info.SpecJSON = nil
183+
info.SpecJSONBytes = nil
183184

184185
// validate!
185186
valid, errors := ValidateOpenAPIDocument(doc)
@@ -201,3 +202,80 @@ info:
201202
// Pre-validation errors should not have SchemaValidationErrors
202203
assert.Empty(t, validationError.SchemaValidationErrors)
203204
}
205+
206+
func TestValidateDocument_WithPrecompiledSchema(t *testing.T) {
207+
petstore, _ := os.ReadFile("../test_specs/petstorev3.json")
208+
doc, _ := libopenapi.NewDocument(petstore)
209+
210+
info := doc.GetSpecInfo()
211+
212+
// Pre-compile the schema
213+
options := config.NewValidationOptions()
214+
compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options)
215+
assert.NoError(t, err)
216+
217+
// Validate with precompiled schema
218+
valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema)
219+
assert.True(t, valid)
220+
assert.Len(t, errs, 0)
221+
222+
// Validate without precompiled schema (should produce identical results)
223+
valid2, errs2 := ValidateOpenAPIDocument(doc)
224+
assert.True(t, valid2)
225+
assert.Len(t, errs2, 0)
226+
}
227+
228+
func TestValidateDocument_WithPrecompiledSchema_Invalid(t *testing.T) {
229+
petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml")
230+
doc, _ := libopenapi.NewDocument(petstore)
231+
232+
info := doc.GetSpecInfo()
233+
234+
// Pre-compile the schema
235+
options := config.NewValidationOptions()
236+
compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options)
237+
assert.NoError(t, err)
238+
239+
// Validate with precompiled schema
240+
valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema)
241+
assert.False(t, valid)
242+
assert.Len(t, errs, 1)
243+
assert.Len(t, errs[0].SchemaValidationErrors, 6)
244+
245+
// Validate without precompiled schema (should produce identical error count)
246+
valid2, errs2 := ValidateOpenAPIDocument(doc)
247+
assert.False(t, valid2)
248+
assert.Len(t, errs2, 1)
249+
assert.Len(t, errs2[0].SchemaValidationErrors, 6)
250+
}
251+
252+
func TestValidateDocument_SpecJSONBytesPath(t *testing.T) {
253+
petstore, _ := os.ReadFile("../test_specs/petstorev3.json")
254+
doc, _ := libopenapi.NewDocument(petstore)
255+
256+
info := doc.GetSpecInfo()
257+
258+
// Nil out SpecJSON but leave SpecJSONBytes intact — forces the SpecJSONBytes path
259+
assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi")
260+
info.SpecJSON = nil
261+
262+
valid, errs := ValidateOpenAPIDocument(doc)
263+
assert.True(t, valid)
264+
assert.Len(t, errs, 0)
265+
}
266+
267+
func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) {
268+
petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml")
269+
doc, _ := libopenapi.NewDocument(petstore)
270+
271+
info := doc.GetSpecInfo()
272+
273+
// Nil out SpecJSON but leave SpecJSONBytes intact
274+
assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi")
275+
info.SpecJSON = nil
276+
277+
valid, errs := ValidateOpenAPIDocument(doc)
278+
assert.False(t, valid)
279+
assert.Len(t, errs, 1)
280+
assert.NotEmpty(t, errs[0].SchemaValidationErrors)
281+
}

0 commit comments

Comments
 (0)