Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 2 additions & 8 deletions parameters/cookie_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parameters

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
Expand Down Expand Up @@ -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

Expand Down
19 changes: 4 additions & 15 deletions parameters/header_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parameters

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
}
Expand Down
19 changes: 4 additions & 15 deletions parameters/path_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package parameters

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
17 changes: 4 additions & 13 deletions parameters/query_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))
}
}
Expand Down
138 changes: 107 additions & 31 deletions parameters/validate_parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Comment thread
daveshanley marked this conversation as resolved.
Schema: schema,
RenderedJSON: jsonSchema,
CompiledSchema: jsch,
})
}
}

// Validate the object and report any errors.
Expand All @@ -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 {
Comment thread
daveshanley marked this conversation as resolved.
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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes GetRenderedSchema stateful. Cache hits return raw JSON via string(cached.RenderedJSON), cache misses call json.Marshal on the []byte from RenderInline()

While testing, The first invalid request returned

ReferenceSchema="\"dHlwZTogInN0cmluZyIKZW51bToKICAgIC0gImEiCiAgICAtICJiIgo=\"", 

then after one successful request warmed the cache, the same invalid request returned

ReferenceSchema="{\"enum\":[\"a\",\"b\"],\"type\":\"string\"}". 

Same input should not change error payloads based on cache state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this!

This actually sent me down the rabbit hole regarding the format of the SchemaValidationFailure.ReferenceSchema attributes on validation errors....

It looks like for most validation errors, such as request body validation errors (REF), we are using the value of RenderInline(), which is plain YAML text. However, in the case of parameter validation, we apply json.Marshal() on this data, which produces some base64 encoded value like you have in your comment example. Correct me if I'm wrong since I might be missing some context here, but looking at the codebase I could not find any rationale for this json.Marshal() transform on YAML data for populating ReferenceSchema on validation errors for parameter validation only. It feels to me this is a pre-existing bug.

So coming back to your original comment, I did two things:

  • I fixed the inconsistency in the return value of GetRenderedSchema based on the state of the cache (whether its a cache hit or miss will return the same value).
  • I removed the json.Marshal() call to align the format of the SchemaValidationFailure.ReferenceSchema for parameter validation errors with the rest of the codebase. Now ReferenceSchema is a plain YAML string, like it is for e.g. request body validation errors.

This was implemented in 97ddd74.

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.
//
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading