From 9e74f57aed45e0196b08bcda9ef0c958950759a1 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Fri, 27 Feb 2026 09:05:58 -0500 Subject: [PATCH 1/3] Add ability to pass in pre-compiled schemas --- schema_validation/validate_document.go | 64 ++++++++++++----- schema_validation/validate_document_test.go | 80 ++++++++++++++++++++- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 635f7335..1a914d2e 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -4,6 +4,7 @@ package schema_validation import ( + "bytes" "encoding/json" "errors" "fmt" @@ -29,14 +30,22 @@ func normalizeJSON(data any) any { // ValidateOpenAPIDocument will validate an OpenAPI document against the OpenAPI 2, 3.0 and 3.1 schemas (depending on version) // It will return true if the document is valid, false if it is not and a slice of ValidationError pointers. func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bool, []*liberrors.ValidationError) { + return ValidateOpenAPIDocumentWithPrecompiled(doc, nil, opts...) +} + +// ValidateOpenAPIDocumentWithPrecompiled validates an OpenAPI document against the OAS JSON Schema. +// When compiledSchema is non-nil it is used directly, skipping schema compilation. +// When SpecJSONBytes is available on the document's SpecInfo, the normalizeJSON round-trip is +// bypassed in favour of a single jsonschema.UnmarshalJSON call. +func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSchema *jsonschema.Schema, opts ...config.Option) (bool, []*liberrors.ValidationError) { options := config.NewValidationOptions(opts...) info := doc.GetSpecInfo() loadedSchema := info.APISchema var validationErrors []*liberrors.ValidationError - // Check if SpecJSON is nil before dereferencing - if info.SpecJSON == nil { + // Check if both JSON representations are nil before proceeding + if info.SpecJSON == nil && info.SpecJSONBytes == nil { validationErrors = append(validationErrors, &liberrors.ValidationError{ ValidationType: helpers.Schema, ValidationSubType: "document", @@ -50,27 +59,44 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo return false, validationErrors } - decodedDocument := *info.SpecJSON + // Use the precompiled schema if provided, otherwise compile it + jsch := compiledSchema + if jsch == nil { + var err error + jsch, err = helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) + if err != nil { + validationErrors = append(validationErrors, &liberrors.ValidationError{ + ValidationType: helpers.Schema, + ValidationSubType: "compilation", + Message: "OpenAPI document schema compilation failed", + Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), + SpecLine: 1, + SpecCol: 0, + HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: loadedSchema, + }) + return false, validationErrors + } + } - // Compile the JSON Schema - jsch, err := helpers.NewCompiledSchema("schema", []byte(loadedSchema), options) - if err != nil { - // schema compilation failed, return validation error instead of panicking - validationErrors = append(validationErrors, &liberrors.ValidationError{ - ValidationType: helpers.Schema, - ValidationSubType: "compilation", - Message: "OpenAPI document schema compilation failed", - Reason: fmt.Sprintf("The OpenAPI schema failed to compile: %s", err.Error()), - SpecLine: 1, - SpecCol: 0, - HowToFix: "check the OpenAPI schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: loadedSchema, - }) - return false, validationErrors + // Build the normalized document value for validation. + // Prefer SpecJSONBytes (single unmarshal) over SpecJSON (marshal+unmarshal round-trip). + var normalized any + if info.SpecJSONBytes != nil && len(*info.SpecJSONBytes) > 0 { + var err error + normalized, err = jsonschema.UnmarshalJSON(bytes.NewReader(*info.SpecJSONBytes)) + if err != nil { + // Fall back to normalizeJSON if UnmarshalJSON fails + if info.SpecJSON != nil { + normalized = normalizeJSON(*info.SpecJSON) + } + } + } else if info.SpecJSON != nil { + normalized = normalizeJSON(*info.SpecJSON) } // Validate the document - scErrs := jsch.Validate(normalizeJSON(decodedDocument)) + scErrs := jsch.Validate(normalized) var schemaValidationErrors []*liberrors.SchemaValidationFailure diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index 3f454376..ee523027 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -177,9 +177,10 @@ info: doc, _ := libopenapi.NewDocument([]byte(spec)) - // Simulate the nil SpecJSON scenario by setting it to nil + // Simulate the nil SpecJSON scenario by setting both to nil info := doc.GetSpecInfo() info.SpecJSON = nil + info.SpecJSONBytes = nil // validate! valid, errors := ValidateOpenAPIDocument(doc) @@ -201,3 +202,80 @@ info: // Pre-validation errors should not have SchemaValidationErrors assert.Empty(t, validationError.SchemaValidationErrors) } + +func TestValidateDocument_WithPrecompiledSchema(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Pre-compile the schema + options := config.NewValidationOptions() + compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options) + assert.NoError(t, err) + + // Validate with precompiled schema + valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema) + assert.True(t, valid) + assert.Len(t, errs, 0) + + // Validate without precompiled schema (should produce identical results) + valid2, errs2 := ValidateOpenAPIDocument(doc) + assert.True(t, valid2) + assert.Len(t, errs2, 0) +} + +func TestValidateDocument_WithPrecompiledSchema_Invalid(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Pre-compile the schema + options := config.NewValidationOptions() + compiledSchema, err := helpers.NewCompiledSchema("schema", []byte(info.APISchema), options) + assert.NoError(t, err) + + // Validate with precompiled schema + valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema) + assert.False(t, valid) + assert.Len(t, errs, 1) + assert.Len(t, errs[0].SchemaValidationErrors, 6) + + // Validate without precompiled schema (should produce identical error count) + valid2, errs2 := ValidateOpenAPIDocument(doc) + assert.False(t, valid2) + assert.Len(t, errs2, 1) + assert.Len(t, errs2[0].SchemaValidationErrors, 6) +} + +func TestValidateDocument_SpecJSONBytesPath(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Nil out SpecJSON but leave SpecJSONBytes intact — forces the SpecJSONBytes path + assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi") + info.SpecJSON = nil + + valid, errs := ValidateOpenAPIDocument(doc) + assert.True(t, valid) + assert.Len(t, errs, 0) +} + +func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Nil out SpecJSON but leave SpecJSONBytes intact + assert.NotNil(t, info.SpecJSONBytes, "SpecJSONBytes should be populated by libopenapi") + info.SpecJSON = nil + + valid, errs := ValidateOpenAPIDocument(doc) + assert.False(t, valid) + assert.Len(t, errs, 1) + assert.NotEmpty(t, errs[0].SchemaValidationErrors) +} From d8378f85ef3b2fd278e0b602059638b67f569454 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Fri, 27 Feb 2026 09:17:39 -0500 Subject: [PATCH 2/3] bump coverage --- schema_validation/validate_document_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index ee523027..89c2f665 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -264,6 +264,25 @@ func TestValidateDocument_SpecJSONBytesPath(t *testing.T) { assert.Len(t, errs, 0) } +func TestValidateDocument_SpecJSONBytesCorrupt_NilSpecJSON(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Put corrupt bytes in SpecJSONBytes so UnmarshalJSON fails, + // and nil out SpecJSON so the fallback normalizeJSON path is skipped. + // This exercises the nil guard on SpecJSON inside the error branch. + corrupt := []byte(`{not valid json!!!}`) + info.SpecJSONBytes = &corrupt + info.SpecJSON = nil + + // Validation should still run (against nil normalized value) and report errors + valid, errs := ValidateOpenAPIDocument(doc) + assert.False(t, valid) + assert.NotEmpty(t, errs) +} + func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") doc, _ := libopenapi.NewDocument(petstore) From 798d5462c020c2bf196a6031ab93c74633d844cb Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Fri, 27 Feb 2026 09:24:24 -0500 Subject: [PATCH 3/3] bump that coverage! --- schema_validation/validate_document_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index 89c2f665..94cbd962 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -283,6 +283,23 @@ func TestValidateDocument_SpecJSONBytesCorrupt_NilSpecJSON(t *testing.T) { assert.NotEmpty(t, errs) } +func TestValidateDocument_SpecJSONBytesCorrupt_FallbackToSpecJSON(t *testing.T) { + petstore, _ := os.ReadFile("../test_specs/petstorev3.json") + doc, _ := libopenapi.NewDocument(petstore) + + info := doc.GetSpecInfo() + + // Put corrupt bytes in SpecJSONBytes so UnmarshalJSON fails, + // but leave SpecJSON intact so the fallback to normalizeJSON executes. + corrupt := []byte(`{not valid json!!!}`) + info.SpecJSONBytes = &corrupt + + // Should still validate successfully via the SpecJSON fallback + valid, errs := ValidateOpenAPIDocument(doc) + assert.True(t, valid) + assert.Len(t, errs, 0) +} + func TestValidateDocument_SpecJSONBytesPath_Invalid(t *testing.T) { petstore, _ := os.ReadFile("../test_specs/invalid_31.yaml") doc, _ := libopenapi.NewDocument(petstore)