diff --git a/parameters/cookie_parameters.go b/parameters/cookie_parameters.go index 46d7a2a..c92883a 100644 --- a/parameters/cookie_parameters.go +++ b/parameters/cookie_parameters.go @@ -4,7 +4,6 @@ package parameters import ( - "encoding/json" "fmt" "net/http" "strconv" @@ -70,13 +69,8 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request, sch = p.Schema.Schema() } - // Render schema once for ReferenceSchema field in errors - var renderedSchema string - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + // Get rendered schema for ReferenceSchema field in errors (uses cache if available) + renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type diff --git a/parameters/header_parameters.go b/parameters/header_parameters.go index a924f75..624d07a 100644 --- a/parameters/header_parameters.go +++ b/parameters/header_parameters.go @@ -4,7 +4,6 @@ package parameters import ( - "encoding/json" "fmt" "net/http" "strconv" @@ -60,13 +59,8 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, sch = p.Schema.Schema() } - // Render schema once for ReferenceSchema field in errors - var renderedSchema string - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + // Get rendered schema for ReferenceSchema field in errors (uses cache if available) + renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type @@ -204,15 +198,10 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request, } } else { if p.Required != nil && *p.Required { - // Render schema for missing required parameter + // Get rendered schema for missing required parameter (uses cache if available) var renderedSchema string if p.Schema != nil { - sch := p.Schema.Schema() - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + renderedSchema = GetRenderedSchema(p.Schema.Schema(), v.options) } validationErrors = append(validationErrors, errors.HeaderParameterMissing(p, pathValue, operation, renderedSchema)) } diff --git a/parameters/path_parameters.go b/parameters/path_parameters.go index bf7005c..c5fd7ac 100644 --- a/parameters/path_parameters.go +++ b/parameters/path_parameters.go @@ -4,7 +4,6 @@ package parameters import ( - "encoding/json" "fmt" "net/http" "net/url" @@ -142,13 +141,8 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p // extract the schema from the parameter sch := p.Schema.Schema() - // Render schema once for ReferenceSchema field in errors - var renderedSchema string - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + // Get rendered schema for ReferenceSchema field in errors (uses cache if available) + renderedSchema := GetRenderedSchema(sch, v.options) // check enum (if present) enumCheck := func(decodedValue string) { @@ -309,13 +303,8 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p if sch.Items != nil && sch.Items.IsA() { iSch := sch.Items.A.Schema() - // Render items schema once for ReferenceSchema field in array errors - var renderedItemsSchema string - if iSch != nil { - rendered, _ := iSch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedItemsSchema = string(schemaBytes) - } + // Get rendered items schema for ReferenceSchema field in errors (uses cache if available) + renderedItemsSchema := GetRenderedSchema(iSch, v.options) for n := range iSch.Type { // determine how to explode the array diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 40074d4..23f5a75 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -121,13 +121,8 @@ doneLooking: } } - // Render schema once for ReferenceSchema field in errors - var renderedSchema string - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + // Get rendered schema for ReferenceSchema field in errors (uses cache if available) + renderedSchema := GetRenderedSchema(sch, v.options) pType := sch.Type @@ -263,12 +258,8 @@ doneLooking: break } } - var renderedSchema string - if sch != nil { - rendered, _ := sch.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedSchema = string(schemaBytes) - } + // Get rendered schema for ReferenceSchema field in errors (uses cache if available) + renderedSchema := GetRenderedSchema(sch, v.options) validationErrors = append(validationErrors, errors.QueryParameterMissing(params[p], pathValue, operation, renderedSchema)) } } diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index 1b7de9c..f2554af 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -18,6 +18,7 @@ import ( stdError "errors" + "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" @@ -35,16 +36,41 @@ func ValidateSingleParameterSchema( pathTemplate string, operation string, ) (validationErrors []*errors.ValidationError) { - // Get the JSON Schema for the parameter definition. - jsonSchema, err := buildJsonRender(schema) - if err != nil { - return validationErrors + var jsch *jsonschema.Schema + var jsonSchema []byte + + // Try cache lookup first - avoids expensive schema compilation on each request + if o != nil && o.SchemaCache != nil && schema != nil && schema.GoLow() != nil { + hash := schema.GoLow().Hash() + if cached, ok := o.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { + jsch = cached.CompiledSchema + } } - // Attempt to compile the JSON Schema - jsch, err := helpers.NewCompiledSchema(name, jsonSchema, o) - if err != nil { - return validationErrors + // Cache miss - compile the schema + if jsch == nil { + // Get the JSON Schema for the parameter definition. + var err error + jsonSchema, err = buildJsonRender(schema) + if err != nil { + return validationErrors + } + + // Attempt to compile the JSON Schema + jsch, err = helpers.NewCompiledSchema(name, jsonSchema, o) + if err != nil { + return validationErrors + } + + // Store in cache for future requests + if o != nil && o.SchemaCache != nil && schema != nil && schema.GoLow() != nil { + hash := schema.GoLow().Hash() + o.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedJSON: jsonSchema, + CompiledSchema: jsch, + }) + } } // Validate the object and report any errors. @@ -71,6 +97,28 @@ func buildJsonRender(schema *base.Schema) ([]byte, error) { return utils.ConvertYAMLtoJSON(renderedSchema) } +// GetRenderedSchema returns a JSON string representation of the schema for error messages. +// It first checks the schema cache for a pre-rendered version, falling back to fresh rendering. +// This avoids expensive re-rendering on each validation when the cache is available. +func GetRenderedSchema(schema *base.Schema, opts *config.ValidationOptions) string { + if schema == nil { + return "" + } + + // Try cache lookup first + if opts != nil && opts.SchemaCache != nil && schema.GoLow() != nil { + hash := schema.GoLow().Hash() + if cached, ok := opts.SchemaCache.Load(hash); ok && cached != nil && len(cached.RenderedJSON) > 0 { + return string(cached.RenderedJSON) + } + } + + // Cache miss - render fresh + rendered, _ := schema.RenderInline() + schemaBytes, _ := json.Marshal(rendered) + return string(schemaBytes) +} + // ValidateParameterSchema will validate a parameter against a raw object, or a blob of json/yaml. // It will return a list of validation errors, if any. // @@ -94,13 +142,59 @@ func ValidateParameterSchema( validationOptions *config.ValidationOptions, ) []*errors.ValidationError { var validationErrors []*errors.ValidationError + var jsch *jsonschema.Schema + var jsonSchema []byte - // 1. build a JSON render of the schema. - renderCtx := base.NewInlineRenderContextForValidation() - renderedSchema, _ := schema.RenderInlineWithContext(renderCtx) - jsonSchema, _ := utils.ConvertYAMLtoJSON(renderedSchema) + // Try cache lookup first - avoids expensive schema compilation on each request + if validationOptions != nil && validationOptions.SchemaCache != nil && schema != nil && schema.GoLow() != nil { + hash := schema.GoLow().Hash() + if cached, ok := validationOptions.SchemaCache.Load(hash); ok && cached != nil && cached.CompiledSchema != nil { + jsch = cached.CompiledSchema + } + } - // 2. decode the object into a json blob. + // Cache miss - render and compile the schema + if jsch == nil { + // 1. build a JSON render of the schema. + renderCtx := base.NewInlineRenderContextForValidation() + renderedSchema, _ := schema.RenderInlineWithContext(renderCtx) + referenceSchema := string(renderedSchema) + jsonSchema, _ = utils.ConvertYAMLtoJSON(renderedSchema) + + // 2. create a new json schema compiler and add the schema to it + var err error + jsch, err = helpers.NewCompiledSchema(name, jsonSchema, validationOptions) + if err != nil { + // schema compilation failed, return validation error instead of panicking + validationErrors = append(validationErrors, &errors.ValidationError{ + ValidationType: validationType, + ValidationSubType: subValType, + Message: fmt.Sprintf("%s '%s' failed schema compilation", entity, name), + Reason: fmt.Sprintf("%s '%s' schema compilation failed: %s", + reasonEntity, name, err.Error()), + SpecLine: 1, + SpecCol: 0, + ParameterName: name, + HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", + Context: string(jsonSchema), + }) + return validationErrors + } + + // Store in cache for future requests + if validationOptions != nil && validationOptions.SchemaCache != nil && schema != nil && schema.GoLow() != nil { + hash := schema.GoLow().Hash() + validationOptions.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedInline: renderedSchema, + ReferenceSchema: referenceSchema, + RenderedJSON: jsonSchema, + CompiledSchema: jsch, + }) + } + } + + // 3. decode the object into a json blob. var decodedObj interface{} rawIsMap := false validEncoding := false @@ -125,24 +219,6 @@ func ValidateParameterSchema( } validEncoding = true } - // 3. create a new json schema compiler and add the schema to it - jsch, err := helpers.NewCompiledSchema(name, jsonSchema, validationOptions) - if err != nil { - // schema compilation failed, return validation error instead of panicking - validationErrors = append(validationErrors, &errors.ValidationError{ - ValidationType: validationType, - ValidationSubType: subValType, - Message: fmt.Sprintf("%s '%s' failed schema compilation", entity, name), - Reason: fmt.Sprintf("%s '%s' schema compilation failed: %s", - reasonEntity, name, err.Error()), - SpecLine: 1, - SpecCol: 0, - ParameterName: name, - HowToFix: "check the parameter schema for invalid JSON Schema syntax, complex regex patterns, or unsupported schema constructs", - Context: string(jsonSchema), - }) - return validationErrors - } // 4. validate the object against the schema var scErrs error diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index 9c51395..215f14a 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -7,11 +7,11 @@ import ( "testing" "github.com/pb33f/libopenapi" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" - + "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/helpers" ) @@ -678,3 +678,441 @@ func BenchmarkValidationWithRegexCache(b *testing.B) { validator.ValidatePathParams(req) } } + +// cacheTestSpec is an OpenAPI spec for testing cache behavior +var cacheTestSpec = []byte(`{ + "openapi": "3.1.0", + "info": { + "title": "Cache Test API", + "version": "1.0.0" + }, + "paths": { + "/items/{id}": { + "get": { + "operationId": "getItem", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +}`) + +// Test_ParameterValidation_CacheUsage verifies that parameter validation uses the schema cache. +// This test validates that: +// 1. Cache is populated after the first validation +// 2. Subsequent validations reuse the cached compiled schemas +// 3. Validation still produces correct results when using cached schemas +func Test_ParameterValidation_CacheUsage(t *testing.T) { + doc, err := libopenapi.NewDocument(cacheTestSpec) + require.NoError(t, err, "Failed to create document") + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs, "Failed to build v3 model") + + // Create options with cache (default behavior) + opts := config.NewValidationOptions() + require.NotNil(t, opts.SchemaCache, "Schema cache should be initialized by default") + + validator := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(opts)) + + // First request - should populate cache + req1, _ := http.NewRequest("GET", "/items/abc123?limit=50", nil) + isSuccess1, errors1 := validator.ValidateQueryParams(req1) + assert.True(t, isSuccess1, "First validation should succeed") + assert.Empty(t, errors1, "First validation should have no errors") + + // Count cached entries (should have at least the limit parameter schema) + cacheCount := 0 + opts.SchemaCache.Range(func(key uint64, value *cache.SchemaCacheEntry) bool { + cacheCount++ + return true + }) + assert.Greater(t, cacheCount, 0, "Cache should have entries after first validation") + + // Second request with different valid value - should use cached schema + req2, _ := http.NewRequest("GET", "/items/xyz789?limit=75", nil) + isSuccess2, errors2 := validator.ValidateQueryParams(req2) + assert.True(t, isSuccess2, "Second validation should succeed") + assert.Empty(t, errors2, "Second validation should have no errors") + + // Third request with invalid value - should still use cached schema but fail validation + req3, _ := http.NewRequest("GET", "/items/test?limit=999", nil) + isSuccess3, errors3 := validator.ValidateQueryParams(req3) + assert.False(t, isSuccess3, "Third validation should fail (limit > maximum)") + assert.NotEmpty(t, errors3, "Third validation should have errors") +} + +// Test_ParameterValidation_WithoutCache verifies that validation works when cache is disabled. +func Test_ParameterValidation_WithoutCache(t *testing.T) { + doc, err := libopenapi.NewDocument(cacheTestSpec) + require.NoError(t, err, "Failed to create document") + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs, "Failed to build v3 model") + + // Create options without cache + opts := config.NewValidationOptions(config.WithSchemaCache(nil)) + require.Nil(t, opts.SchemaCache, "Schema cache should be nil") + + validator := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(opts)) + + // Validation should still work without cache + req, _ := http.NewRequest("GET", "/items/abc123?limit=50", nil) + isSuccess, errors := validator.ValidateQueryParams(req) + assert.True(t, isSuccess, "Validation should succeed without cache") + assert.Empty(t, errors, "Validation should have no errors") + + // Validation with invalid value should fail + req2, _ := http.NewRequest("GET", "/items/abc123?limit=999", nil) + isSuccess2, errors2 := validator.ValidateQueryParams(req2) + assert.False(t, isSuccess2, "Validation should fail for invalid value") + assert.NotEmpty(t, errors2, "Validation should report errors") +} + +// Test_ParameterValidation_CacheConsistency verifies that cached schemas produce +// the same validation results as freshly compiled schemas. +func Test_ParameterValidation_CacheConsistency(t *testing.T) { + doc, err := libopenapi.NewDocument(cacheTestSpec) + require.NoError(t, err, "Failed to create document") + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs, "Failed to build v3 model") + + // Run the same validations with and without cache + testCases := []struct { + name string + url string + expected bool + }{ + {"valid_limit", "/items/abc?limit=50", true}, + {"limit_at_max", "/items/abc?limit=100", true}, + {"limit_at_min", "/items/abc?limit=1", true}, + {"limit_too_high", "/items/abc?limit=101", false}, + {"limit_too_low", "/items/abc?limit=0", false}, + } + + // First run with cache + optsWithCache := config.NewValidationOptions() + validatorWithCache := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(optsWithCache)) + + // Second run without cache + optsNoCache := config.NewValidationOptions(config.WithSchemaCache(nil)) + validatorNoCache := NewParameterValidator(&v3Model.Model, config.WithExistingOpts(optsNoCache)) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest("GET", tc.url, nil) + + successWithCache, errorsWithCache := validatorWithCache.ValidateQueryParams(req) + successNoCache, errorsNoCache := validatorNoCache.ValidateQueryParams(req) + + assert.Equal(t, tc.expected, successWithCache, "Cached validation result mismatch for %s", tc.name) + assert.Equal(t, successWithCache, successNoCache, "Cache vs no-cache results should match for %s", tc.name) + assert.Equal(t, len(errorsWithCache), len(errorsNoCache), "Error count should match for %s", tc.name) + }) + } +} + +// Test_GetRenderedSchema_NilSchema verifies GetRenderedSchema handles nil schema gracefully. +func Test_GetRenderedSchema_NilSchema(t *testing.T) { + opts := config.NewValidationOptions() + result := GetRenderedSchema(nil, opts) + assert.Empty(t, result, "GetRenderedSchema should return empty string for nil schema") +} + +// Test_GetRenderedSchema_NilOptions verifies GetRenderedSchema works without options. +func Test_GetRenderedSchema_NilOptions(t *testing.T) { + // Parse a document to get a properly initialized schema + spec := []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": { + "/test": { + "get": { + "parameters": [{ + "name": "id", + "in": "query", + "schema": {"type": "string", "minLength": 1} + }], + "responses": {"200": {"description": "OK"}} + } + } + } + }`) + + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + // Get the parameter schema + pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") + param := pathItem.Get.Parameters[0] + schema := param.Schema.Schema() + + // Call with nil options - should still render the schema (returns some representation) + result := GetRenderedSchema(schema, nil) + assert.NotEmpty(t, result, "GetRenderedSchema should render schema even with nil options") +} + +// Test_GetRenderedSchema_CacheHit verifies GetRenderedSchema uses cached data when available. +func Test_GetRenderedSchema_CacheHit(t *testing.T) { + spec := []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": { + "/test": { + "get": { + "parameters": [{ + "name": "id", + "in": "query", + "schema": {"type": "integer", "minimum": 1} + }], + "responses": {"200": {"description": "OK"}} + } + } + } + }`) + + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + // Get the parameter schema + pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") + param := pathItem.Get.Parameters[0] + schema := param.Schema.Schema() + + // Create options with cache and pre-populate with known value + opts := config.NewValidationOptions() + hash := schema.GoLow().Hash() + testCachedJSON := []byte(`{"type":"integer","minimum":1,"cached":true}`) + opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedJSON: testCachedJSON, + }) + + // GetRenderedSchema should return the cached value + result := GetRenderedSchema(schema, opts) + assert.Equal(t, string(testCachedJSON), result, "GetRenderedSchema should return cached JSON") +} + +// Test_GetRenderedSchema_NilCache verifies GetRenderedSchema works when cache is disabled. +func Test_GetRenderedSchema_NilCache(t *testing.T) { + spec := []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": { + "/test": { + "get": { + "parameters": [{ + "name": "id", + "in": "query", + "schema": {"type": "boolean"} + }], + "responses": {"200": {"description": "OK"}} + } + } + } + }`) + + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + // Get the parameter schema + pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") + param := pathItem.Get.Parameters[0] + schema := param.Schema.Schema() + + // Create options with cache disabled + opts := config.NewValidationOptions(config.WithSchemaCache(nil)) + require.Nil(t, opts.SchemaCache) + + // GetRenderedSchema should still work by rendering fresh (returns some representation) + result := GetRenderedSchema(schema, opts) + assert.NotEmpty(t, result, "GetRenderedSchema should render schema even with nil cache") +} + +// Test_GetRenderedSchema_CacheMiss verifies GetRenderedSchema renders fresh when cache entry has empty RenderedJSON. +// This tests the code path where cache lookup succeeds but RenderedJSON is empty. +func Test_GetRenderedSchema_CacheMiss(t *testing.T) { + spec := []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": { + "/test": { + "get": { + "parameters": [{ + "name": "id", + "in": "query", + "schema": {"type": "integer"} + }], + "responses": {"200": {"description": "OK"}} + } + } + } + }`) + + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + // Get the parameter schema + pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test") + param := pathItem.Get.Parameters[0] + schema := param.Schema.Schema() + + // Create options with cache enabled + opts := config.NewValidationOptions() + require.NotNil(t, opts.SchemaCache) + + // Store an entry with empty RenderedJSON to simulate cache miss scenario + hash := schema.GoLow().Hash() + opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + RenderedJSON: nil, // Empty - should trigger fresh rendering + }) + + // GetRenderedSchema should render fresh since RenderedJSON is empty + result := GetRenderedSchema(schema, opts) + assert.NotEmpty(t, result, "GetRenderedSchema should render fresh when cached RenderedJSON is empty") +} + +// Test_ValidateSingleParameterSchema_CacheMissCompiledSchema tests the path where cache entry +// exists but CompiledSchema is nil, forcing recompilation. +func Test_ValidateSingleParameterSchema_CacheMissCompiledSchema(t *testing.T) { + spec := []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0.0"}, + "paths": { + "/test/{id}": { + "get": { + "parameters": [{ + "name": "id", + "in": "path", + "required": true, + "schema": {"type": "integer", "minimum": 1} + }], + "responses": {"200": {"description": "OK"}} + } + } + } + }`) + + doc, err := libopenapi.NewDocument(spec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + // Get the parameter schema + pathItem := v3Model.Model.Paths.PathItems.GetOrZero("/test/{id}") + param := pathItem.Get.Parameters[0] + schema := param.Schema.Schema() + + // Create options with cache enabled + opts := config.NewValidationOptions() + require.NotNil(t, opts.SchemaCache) + + // Store an entry with nil CompiledSchema to force recompilation + hash := schema.GoLow().Hash() + opts.SchemaCache.Store(hash, &cache.SchemaCacheEntry{ + Schema: schema, + CompiledSchema: nil, // nil - should trigger recompilation + }) + + // Validate should still work by recompiling the schema + result := ValidateSingleParameterSchema( + schema, + int64(5), // valid integer + "Path parameter", + "The path parameter", + "id", + helpers.ParameterValidation, + helpers.ParameterValidationPath, + opts, + "/test/{id}", + "get", + ) + assert.Empty(t, result, "Validation should pass for valid integer") + + // Now verify the cache was populated with the compiled schema + cached, ok := opts.SchemaCache.Load(hash) + assert.True(t, ok, "Cache entry should exist") + assert.NotNil(t, cached.CompiledSchema, "CompiledSchema should be populated after validation") +} + +// arrayValidationSpec is used to test array parameter validation with the updated function signatures +var arrayValidationSpec = []byte(`{ + "openapi": "3.1.0", + "info": {"title": "Array Test", "version": "1.0.0"}, + "paths": { + "/test": { + "get": { + "parameters": [{ + "name": "ids", + "in": "query", + "schema": { + "type": "array", + "items": {"type": "integer", "minimum": 1} + } + }], + "responses": {"200": {"description": "OK"}} + } + } + } +}`) + +// Test_ArrayValidation_ErrorContainsRenderedSchema verifies that array validation errors +// still contain the rendered schema after the rendering optimization. +func Test_ArrayValidation_ErrorContainsRenderedSchema(t *testing.T) { + doc, err := libopenapi.NewDocument(arrayValidationSpec) + require.NoError(t, err) + + v3Model, errs := doc.BuildV3Model() + require.Nil(t, errs) + + validator := NewParameterValidator(&v3Model.Model) + + // Request with invalid array values (strings instead of integers) + req, _ := http.NewRequest("GET", "/test?ids=abc,def", nil) + + success, validationErrors := validator.ValidateQueryParams(req) + assert.False(t, success, "Validation should fail for non-integer array values") + assert.NotEmpty(t, validationErrors, "Should have validation errors") + + // Verify error message is properly formatted + assert.Contains(t, validationErrors[0].Message, "ids", "Error should reference parameter name") +} diff --git a/parameters/validation_functions.go b/parameters/validation_functions.go index f931588..ff4749e 100644 --- a/parameters/validation_functions.go +++ b/parameters/validation_functions.go @@ -119,12 +119,8 @@ func ValidateQueryArray( var validationErrors []*errors.ValidationError itemsSchema := sch.Items.A.Schema() - var renderedItemsSchema string - if itemsSchema != nil { - rendered, _ := itemsSchema.RenderInline() - schemaBytes, _ := json.Marshal(rendered) - renderedItemsSchema = string(schemaBytes) - } + // Get rendered items schema for ReferenceSchema field in errors (uses cache if available) + renderedItemsSchema := GetRenderedSchema(itemsSchema, validationOptions) // check for an exploded bit on the schema. // if it's exploded, then we need to check each item in the array