Skip to content

Enable 'Format' and 'Content' assertions during schema validation #116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 14, 2025
27 changes: 24 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import "github.com/santhosh-tekuri/jsonschema/v6"
//
// Generally fluent With... style functions are used to establish the desired behavior.
type ValidationOptions struct {
RegexEngine jsonschema.RegexpEngine
RegexEngine jsonschema.RegexpEngine
FormatAssertions bool
ContentAssertions bool
}

// Option Enables an 'Options pattern' approach
Expand All @@ -15,11 +17,16 @@ type Option func(*ValidationOptions)
// NewValidationOptions creates a new ValidationOptions instance with default values.
func NewValidationOptions(opts ...Option) *ValidationOptions {
// Create the set of default values
o := &ValidationOptions{}
o := &ValidationOptions{
FormatAssertions: false,
ContentAssertions: false,
}

// Apply any supplied overrides
for _, opt := range opts {
opt(o)
if opt != nil {
opt(o)
}
}

// Done
Expand All @@ -32,3 +39,17 @@ func WithRegexEngine(engine jsonschema.RegexpEngine) Option {
o.RegexEngine = engine
}
}

// WithFormatAssertions enables checks for 'format' assertions (such as date, date-time, uuid, etc)
func WithFormatAssertions() Option {
return func(o *ValidationOptions) {
o.FormatAssertions = true
}
}

// WithContentAssertions enables checks for contentType, contentEncoding, etc
func WithContentAssertions() Option {
return func(o *ValidationOptions) {
o.ContentAssertions = true
}
}
73 changes: 73 additions & 0 deletions helpers/schema_compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package helpers

import (
"bytes"
"fmt"

"github.com/santhosh-tekuri/jsonschema/v6"

"github.com/pb33f/libopenapi-validator/config"
)

// ConfigureCompiler configures a JSON Schema compiler with the desired behavior.
func ConfigureCompiler(c *jsonschema.Compiler, o *config.ValidationOptions) {
if o == nil {
// Sanity
return
}

// nil is the default so this is OK.
c.UseRegexpEngine(o.RegexEngine)

// Enable Format assertions if required.
if o.FormatAssertions {
c.AssertFormat()
}

// Content Assertions
if o.ContentAssertions {
c.AssertContent()
}
}

// NewCompilerWithOptions mints a new JSON schema compiler with custom configuration.
func NewCompilerWithOptions(o *config.ValidationOptions) *jsonschema.Compiler {
// Build it
c := jsonschema.NewCompiler()

// Configure it
ConfigureCompiler(c, o)

// Return it
return c
}

// NewCompiledSchema establishes a programmatic representation of a JSON Schema document that is used for validation.
func NewCompiledSchema(name string, jsonSchema []byte, o *config.ValidationOptions) (*jsonschema.Schema, error) {
// Fake-Up a resource name for the schema
resourceName := fmt.Sprintf("%s.json", name)

// Establish a compiler with the desired configuration
compiler := NewCompilerWithOptions(o)
compiler.UseLoader(NewCompilerLoader())

// Decode the JSON Schema into a JSON blob.
decodedSchema, err := jsonschema.UnmarshalJSON(bytes.NewReader(jsonSchema))
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON schema: %w", err)
}

// Give our schema to the compiler.
if err = compiler.AddResource(resourceName, decodedSchema); err != nil {
return nil, fmt.Errorf("failed to add resource to schema compiler: %w", err)
}

Check warning on line 63 in helpers/schema_compiler.go

View check run for this annotation

Codecov / codecov/patch

helpers/schema_compiler.go#L62-L63

Added lines #L62 - L63 were not covered by tests

// Try to compile it.
jsch, err := compiler.Compile(resourceName)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema: %w", err)
}

// Done.
return jsch, nil
}
113 changes: 113 additions & 0 deletions helpers/schema_compiler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package helpers

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/pb33f/libopenapi-validator/config"
)

// A few simple JSON Schemas
const stringSchema = `{
"type": "string",
"format": "date",
"minLength": 10
}`

const objectSchema = `{
"type": "object",
"title" : "Fish",
"properties" : {
"name" : {
"type": "string",
"description": "The given name of the fish"
},
"species" : {
"type" : "string",
"enum" : [ "OTHER", "GUPPY", "PIKE", "BASS" ]
}
}
}`

func Test_SchemaWithNilOptions(t *testing.T) {
jsch, err := NewCompiledSchema("test", []byte(stringSchema), nil)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_SchemaWithDefaultOptions(t *testing.T) {
valOptions := config.NewValidationOptions()
jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_SchemaWithOptions(t *testing.T) {
valOptions := config.NewValidationOptions(config.WithFormatAssertions(), config.WithContentAssertions())

jsch, err := NewCompiledSchema("test", []byte(stringSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_ObjectSchema(t *testing.T) {
valOptions := config.NewValidationOptions()
jsch, err := NewCompiledSchema("test", []byte(objectSchema), valOptions)

require.NoError(t, err, "Failed to compile Schema")
require.NotNil(t, jsch, "Did not return a compiled schema")
}

func Test_ValidJSONSchemaWithInvalidContent(t *testing.T) {
// An example of a dubious JSON Schema
const badSchema = `{
"type": "you-dont-know-me",
"format": "date",
"minLength": 10
}`

jsch, err := NewCompiledSchema("test", []byte(badSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}

func Test_MalformedSONSchema(t *testing.T) {
// An example of a JSON schema with malformed JSON
const badSchema = `{
"type": "you-dont-know-me",
"format": "date"
"minLength": 10
}`

jsch, err := NewCompiledSchema("test", []byte(badSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}

func Test_ValidJSONSchemaWithIncompleteContent(t *testing.T) {
// An example of a dJSON schema with references to non-existent stuff
const incompleteSchema = `{
"type": "object",
"title" : "unresolvable",
"properties" : {
"name" : {
"type": "string",
},
"species" : {
"$ref": "#/$defs/speciesEnum"
}
}
}`

jsch, err := NewCompiledSchema("test", []byte(incompleteSchema), nil)

assert.Error(t, err, "Expected an error to be thrown")
assert.Nil(t, jsch, "invalid schema compiled!")
}
3 changes: 2 additions & 1 deletion parameters/cookie_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func (v *paramValidator) ValidateCookieParamsWithPathItem(request *http.Request,
"The cookie parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery,
v.options)...)
}
}
case helpers.Array:
Expand Down
2 changes: 1 addition & 1 deletion parameters/header_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request,
"The header parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
}

case helpers.Array:
Expand Down
4 changes: 3 additions & 1 deletion parameters/path_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
v.options,
)...)

case helpers.Integer, helpers.Number:
Expand All @@ -161,6 +162,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath,
v.options,
)...)

case helpers.Boolean:
Expand Down Expand Up @@ -221,7 +223,7 @@ func (v *paramValidator) ValidatePathParamsWithPathItem(request *http.Request, p
"The path parameter",
p.Name,
helpers.ParameterValidation,
helpers.ParameterValidationPath)...)
helpers.ParameterValidationPath, v.options)...)
}

case helpers.Array:
Expand Down
7 changes: 4 additions & 3 deletions parameters/query_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ doneLooking:
"The query parameter",
params[p].Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
if len(validationErrors) > numErrors {
// we've already added an error for this, so we can skip the rest of the values
break skipValues
Expand All @@ -185,7 +185,7 @@ doneLooking:
// only check if items is a schema, not a boolean
if sch.Items != nil && sch.Items.IsA() {
validationErrors = append(validationErrors,
ValidateQueryArray(sch, params[p], ef, contentWrapped)...)
ValidateQueryArray(sch, params[p], ef, contentWrapped, v.options)...)
}
}
}
Expand All @@ -209,7 +209,7 @@ doneLooking:
"The query parameter (which is an array)",
params[p].Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery)...)
helpers.ParameterValidationQuery, v.options)...)
break doneLooking
}
}
Expand Down Expand Up @@ -252,5 +252,6 @@ func (v *paramValidator) validateSimpleParam(sch *base.Schema, rawParam string,
parameter.Name,
helpers.ParameterValidation,
helpers.ParameterValidationQuery,
v.options,
)
}
Loading