From 34eae02a7511e4551dec993fd962f46a54b075f2 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Tue, 6 Oct 2026 10:18:13 -0400 Subject: [PATCH 1/2] fix: use the maintained JSON Schema fork and preserve document diagnostics --- README.md | 8 + cache/cache.go | 2 +- cache/cache_test.go | 2 +- cmd/validate/main.go | 2 +- config/config.go | 2 +- config/config_test.go | 2 +- errors/validation_error.go | 4 +- go.mod | 4 +- go.sum | 8 +- helpers/path_finder.go | 4 +- helpers/path_finder_test.go | 4 +- helpers/schema_compiler.go | 2 +- helpers/schema_output.go | 2 +- helpers/schema_output_test.go | 2 +- helpers/url_loader.go | 4 +- openapi_vocabulary/coercion.go | 4 +- openapi_vocabulary/coercion_simple_test.go | 4 +- openapi_vocabulary/discriminator.go | 4 +- openapi_vocabulary/metadata.go | 4 +- openapi_vocabulary/nullable.go | 4 +- openapi_vocabulary/vocabulary.go | 4 +- openapi_vocabulary/vocabulary_test.go | 4 +- parameters/validate_parameter.go | 2 +- parameters/validate_parameter_test.go | 2 +- requests/validate_request.go | 2 +- responses/validate_response.go | 2 +- schema_validation/document_property_names.go | 66 +++++++ .../document_property_names_test.go | 161 ++++++++++++++++++ schema_validation/property_locator.go | 4 +- schema_validation/schema_resources.go | 2 +- schema_validation/validate_document.go | 26 ++- schema_validation/validate_document_test.go | 17 +- schema_validation/validate_schema.go | 2 +- .../validate_schema_extract_errors_test.go | 4 +- strict/matcher.go | 4 +- strict/types.go | 2 +- validator_test.go | 2 +- 37 files changed, 317 insertions(+), 61 deletions(-) create mode 100644 schema_validation/document_property_names.go create mode 100644 schema_validation/document_property_names_test.go diff --git a/README.md b/README.md index cf758339..96ae512b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ Document validation is explicitly covered for OpenAPI 3.0, 3.1, and 3.2. 👉👉 [Check out the full documentation](https://pb33f.io/libopenapi/validation/) 👈👈 +## JSON Schema dependency + +The validator uses the maintained [`pb33f/jsonschema/v6`](https://github.com/pb33f/jsonschema) fork. +If you pass compiled schemas, implement a schema cache, configure a regex engine, or inspect +`OriginalJsonSchemaError`, update direct imports from `github.com/santhosh-tekuri/jsonschema/v6` +to `github.com/pb33f/jsonschema/v6` (including `/kind` imports). Types from the two module +paths have different Go identities. Standard validator calls need no code changes. + ## Runtime validation features The validator now includes a standalone OpenAPI router, authentication callbacks with route context and replayable bodies, immutable custom body codecs, opt-in request defaults, `Parameter.content` decoding, validation policy switches, and explicit OpenAPI 3.2 document validation. diff --git a/cache/cache.go b/cache/cache.go index 6130711d..ccd6dd45 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -5,8 +5,8 @@ package cache import ( "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" ) // SchemaCacheEntry holds a compiled schema and its intermediate representations. diff --git a/cache/cache_test.go b/cache/cache_test.go index 821182f5..488669e9 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -7,10 +7,10 @@ import ( "sync" "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/testify/assert" "github.com/pb33f/testify/require" - "github.com/santhosh-tekuri/jsonschema/v6" ) func TestNewDefaultCache(t *testing.T) { diff --git a/cmd/validate/main.go b/cmd/validate/main.go index ec1c626b..346a9411 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -9,8 +9,8 @@ import ( "github.com/dlclark/regexp2" "github.com/goccy/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi" - "github.com/santhosh-tekuri/jsonschema/v6" validator "github.com/pb33f/libopenapi-validator" "github.com/pb33f/libopenapi-validator/config" diff --git a/config/config.go b/config/config.go index 806774f3..f5aae1c6 100644 --- a/config/config.go +++ b/config/config.go @@ -8,8 +8,8 @@ import ( "log/slog" "net/http" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" diff --git a/config/config_test.go b/config/config_test.go index b9cfc728..72544f0b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,10 +9,10 @@ import ( "sync" "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/testify/assert" "github.com/pb33f/testify/require" - "github.com/santhosh-tekuri/jsonschema/v6" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" diff --git a/errors/validation_error.go b/errors/validation_error.go index 38ac82f5..7e8e91d8 100644 --- a/errors/validation_error.go +++ b/errors/validation_error.go @@ -1,4 +1,4 @@ -// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package errors @@ -6,8 +6,8 @@ package errors import ( "fmt" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi-validator/helpers" - "github.com/santhosh-tekuri/jsonschema/v6" ) // SchemaValidationFailure describes any failure that occurs when validating data diff --git a/go.mod b/go.mod index 80f0e343..b466ad07 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/goccy/go-yaml v1.19.2 github.com/pb33f/go-yaml v0.1.1 github.com/pb33f/jsonpath v0.8.4 - github.com/pb33f/libopenapi v0.41.2 + github.com/pb33f/jsonschema/v6 v6.0.3 + github.com/pb33f/libopenapi v0.41.3 github.com/pb33f/testify v0.1.1 - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 golang.org/x/text v0.42.0 ) diff --git a/go.sum b/go.sum index 2423c8fd..5ce05984 100644 --- a/go.sum +++ b/go.sum @@ -21,16 +21,16 @@ github.com/pb33f/go-yaml v0.1.1 h1:yWGmhVdwzionRnSc/hEJwcfq6PaZL2QdYzyReXBbX7Q= github.com/pb33f/go-yaml v0.1.1/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo= github.com/pb33f/jsonpath v0.8.4 h1:Yx7fxsKl8scgERL22WhKDs2c6gYNNUonPSWEG8Y194s= github.com/pb33f/jsonpath v0.8.4/go.mod h1:eO6mgdhw5RgzCxm/bXTi75VUs0/cCYJzdGjMCCUEI6M= -github.com/pb33f/libopenapi v0.41.2 h1:7/KNzJJ0o5fC/tYxDYcHjziKfg0bkBpFUfI2UDbChAo= -github.com/pb33f/libopenapi v0.41.2/go.mod h1:mUldESjO6ownR1TCcywvoYp7e4v/bCdQyiiIqsgWne0= +github.com/pb33f/jsonschema/v6 v6.0.3 h1:TMIkE1xCmnF8Cy+anIBs+r4ASfGtwzfFxYsrrIn0wSU= +github.com/pb33f/jsonschema/v6 v6.0.3/go.mod h1:cYPShw+w1Jrs0YgpE2rk1FLdIAsKfWcotv/54G6ddFo= +github.com/pb33f/libopenapi v0.41.3 h1:XfLiw0iHjl2mGBG51SSIU62etiHTYYmGUelFtI7at8M= +github.com/pb33f/libopenapi v0.41.3/go.mod h1:mUldESjO6ownR1TCcywvoYp7e4v/bCdQyiiIqsgWne0= github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ= github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w= github.com/pb33f/testify v0.1.1 h1:mnHe7uxKt8dyNYEGUspow72VjD264DB5rOJq4bz5tJw= github.com/pb33f/testify v0.1.1/go.mod h1:keghMqOLECF1ENzESnfM+cwPcKN5uhTOpvbtjgL6aZg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/helpers/path_finder.go b/helpers/path_finder.go index 29155790..d32e2f23 100644 --- a/helpers/path_finder.go +++ b/helpers/path_finder.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers @@ -8,7 +8,7 @@ import ( "strings" "unicode" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // ExtractJSONPathFromValidationError traverses and processes a ValidationError to construct a JSONPath string representation of its instance location. diff --git a/helpers/path_finder_test.go b/helpers/path_finder_test.go index f6d78c6f..e84fe019 100644 --- a/helpers/path_finder_test.go +++ b/helpers/path_finder_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers @@ -6,8 +6,8 @@ package helpers import ( "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/testify/assert" - "github.com/santhosh-tekuri/jsonschema/v6" ) func TestDiveIntoValidationError(t *testing.T) { diff --git a/helpers/schema_compiler.go b/helpers/schema_compiler.go index c90d9511..0cc374ae 100644 --- a/helpers/schema_compiler.go +++ b/helpers/schema_compiler.go @@ -9,7 +9,7 @@ import ( "sort" "strconv" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi-validator/config" "github.com/pb33f/libopenapi-validator/openapi_vocabulary" diff --git a/helpers/schema_output.go b/helpers/schema_output.go index 811e1d5b..7b65e6a3 100644 --- a/helpers/schema_output.go +++ b/helpers/schema_output.go @@ -3,7 +3,7 @@ package helpers -import "github.com/santhosh-tekuri/jsonschema/v6" +import "github.com/pb33f/jsonschema/v6" // FlattenSchemaOutputErrors returns every output unit that carries an actual validation error. func FlattenSchemaOutputErrors(output *jsonschema.OutputUnit) []jsonschema.OutputUnit { diff --git a/helpers/schema_output_test.go b/helpers/schema_output_test.go index f318ffd5..2072dca1 100644 --- a/helpers/schema_output_test.go +++ b/helpers/schema_output_test.go @@ -6,8 +6,8 @@ package helpers import ( "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/testify/assert" - "github.com/santhosh-tekuri/jsonschema/v6" ) func TestFlattenSchemaOutputErrors(t *testing.T) { diff --git a/helpers/url_loader.go b/helpers/url_loader.go index a5cd66b6..06dbdb9d 100644 --- a/helpers/url_loader.go +++ b/helpers/url_loader.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // https://pb33f.io package helpers @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // HTTPURLLoader is a type that implements the Loader interface for loading schemas from HTTP URLs. diff --git a/openapi_vocabulary/coercion.go b/openapi_vocabulary/coercion.go index 3ba87b84..caebbabc 100644 --- a/openapi_vocabulary/coercion.go +++ b/openapi_vocabulary/coercion.go @@ -1,4 +1,4 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary @@ -7,7 +7,7 @@ import ( "regexp" "strconv" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // coercionExtension handles Jackson-style scalar coercion (string->boolean/number) diff --git a/openapi_vocabulary/coercion_simple_test.go b/openapi_vocabulary/coercion_simple_test.go index 736c9e74..84c5e471 100644 --- a/openapi_vocabulary/coercion_simple_test.go +++ b/openapi_vocabulary/coercion_simple_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary @@ -8,8 +8,8 @@ import ( "strings" "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/testify/assert" - "github.com/santhosh-tekuri/jsonschema/v6" ) func TestCoercion_Vocabulary_CompilationSuccess(t *testing.T) { diff --git a/openapi_vocabulary/discriminator.go b/openapi_vocabulary/discriminator.go index b88ddbba..1bc72afe 100644 --- a/openapi_vocabulary/discriminator.go +++ b/openapi_vocabulary/discriminator.go @@ -1,10 +1,10 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // discriminatorExtension handles the OpenAPI discriminator keyword diff --git a/openapi_vocabulary/metadata.go b/openapi_vocabulary/metadata.go index 04bd0b2d..2f88f95f 100644 --- a/openapi_vocabulary/metadata.go +++ b/openapi_vocabulary/metadata.go @@ -1,10 +1,10 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // exampleExtension handles the OpenAPI example keyword (metadata only) diff --git a/openapi_vocabulary/nullable.go b/openapi_vocabulary/nullable.go index bb63c1f6..e36c7e29 100644 --- a/openapi_vocabulary/nullable.go +++ b/openapi_vocabulary/nullable.go @@ -1,10 +1,10 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // compileNullable compiles the nullable keyword based on OpenAPI version diff --git a/openapi_vocabulary/vocabulary.go b/openapi_vocabulary/vocabulary.go index 452d82a7..4eb0845a 100644 --- a/openapi_vocabulary/vocabulary.go +++ b/openapi_vocabulary/vocabulary.go @@ -1,10 +1,10 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary import ( - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" ) // OpenAPIVocabularyURL is the vocabulary URL for OpenAPI-specific keywords diff --git a/openapi_vocabulary/vocabulary_test.go b/openapi_vocabulary/vocabulary_test.go index a951f16c..6a3a31cd 100644 --- a/openapi_vocabulary/vocabulary_test.go +++ b/openapi_vocabulary/vocabulary_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package openapi_vocabulary @@ -7,8 +7,8 @@ import ( "strings" "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/testify/assert" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/message" ) diff --git a/parameters/validate_parameter.go b/parameters/validate_parameter.go index c4e77990..eaec8c93 100644 --- a/parameters/validate_parameter.go +++ b/parameters/validate_parameter.go @@ -10,8 +10,8 @@ import ( "reflect" "strings" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/language" "golang.org/x/text/message" diff --git a/parameters/validate_parameter_test.go b/parameters/validate_parameter_test.go index 32591734..180e6921 100644 --- a/parameters/validate_parameter_test.go +++ b/parameters/validate_parameter_test.go @@ -12,13 +12,13 @@ import ( "sync" "testing" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel" "github.com/pb33f/libopenapi/datamodel/high/base" lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/pb33f/testify/assert" "github.com/pb33f/testify/require" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/message" "github.com/pb33f/libopenapi-validator/cache" diff --git a/requests/validate_request.go b/requests/validate_request.go index 35970ee8..f5d8a9b7 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -13,8 +13,8 @@ import ( "strconv" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/language" "golang.org/x/text/message" diff --git a/responses/validate_response.go b/responses/validate_response.go index 30ce8ebd..474b0c3f 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -15,8 +15,8 @@ import ( "strconv" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/language" "golang.org/x/text/message" diff --git a/schema_validation/document_property_names.go b/schema_validation/document_property_names.go new file mode 100644 index 00000000..0f0abc40 --- /dev/null +++ b/schema_validation/document_property_names.go @@ -0,0 +1,66 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package schema_validation + +import ( + "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" + "github.com/pb33f/jsonschema/v6/kind" + + liberrors "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi-validator/helpers" +) + +// Index the original property-name errors by their typed kind. Flattened output +// retains these kinds, but its children use locations relative to the key string. +func documentPropertyNameErrors(root *jsonschema.ValidationError) map[*kind.PropertyNames]*jsonschema.ValidationError { + var found map[*kind.PropertyNames]*jsonschema.ValidationError + var visit func(*jsonschema.ValidationError) + visit = func(err *jsonschema.ValidationError) { + if name, ok := err.ErrorKind.(*kind.PropertyNames); ok { + if found == nil { + found = make(map[*kind.PropertyNames]*jsonschema.ValidationError) + } + found[name] = err + return + } + for _, cause := range err.Causes { + visit(cause) + } + } + visit(root) + return found +} + +func documentPropertyNamePattern(err *jsonschema.ValidationError) string { + if pattern, ok := err.ErrorKind.(*kind.Pattern); ok { + return pattern.Want + } + for _, cause := range err.Causes { + if pattern := documentPropertyNamePattern(cause); pattern != "" { + return pattern + } + } + return "" +} + +func applyDocumentPropertyNameError(root *yaml.Node, err *jsonschema.ValidationError, name string, failure *liberrors.SchemaValidationFailure) { + failure.FieldName = name + failure.InstancePath = appendPathSegment(err.InstanceLocation, name) + failure.FieldPath = helpers.ExtractJSONPathFromInstanceLocation(failure.InstancePath) + if pattern := documentPropertyNamePattern(err); pattern != "" { + failure.Reason = buildEnhancedReason(name, pattern) + } else { + failure.Reason = err.Error() + } + parent := root + if len(err.InstanceLocation) > 0 { + parent = LocateSchemaPropertyNodeByJSONPath(root, buildJSONPointer(err.InstanceLocation)) + } + if key := findMapKeyNode(parent, name); key != nil { + failure.Line = key.Line + failure.Column = key.Column + failure.ReferenceObject = key.Value + } +} diff --git a/schema_validation/document_property_names_test.go b/schema_validation/document_property_names_test.go new file mode 100644 index 00000000..4e6b9fb6 --- /dev/null +++ b/schema_validation/document_property_names_test.go @@ -0,0 +1,161 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package schema_validation + +import ( + "fmt" + "testing" + + "github.com/pb33f/jsonschema/v6" + "github.com/pb33f/libopenapi" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" + + liberrors "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/libopenapi-validator/helpers" +) + +func TestDocumentPropertyNamesKeepEachLocation(t *testing.T) { + for _, version := range []string{"3.1.0", "3.2.0"} { + t.Run(version, func(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(fmt.Sprintf(`openapi: %s +info: + title: Property name locations + version: '1' +paths: {} +components: + schemas: + $bad: + type: object + "$a'~/b": + type: string + responses: + $bad: + description: Invalid component name + x-example: + $bad: This extension key is valid +`, version))) + require.NoError(t, err) + valid, failures := ValidateOpenAPIDocument(doc) + require.False(t, valid) + require.Len(t, failures, 1) + require.Len(t, failures[0].SchemaValidationErrors, 3) + byLine := make(map[int]*liberrors.SchemaValidationFailure) + for _, failure := range failures[0].SchemaValidationErrors { + byLine[failure.Line] = failure + } + for _, want := range []struct { + line int + name string + section string + path string + }{ + {8, "$bad", "schemas", "$.components.schemas['$bad']"}, + {10, "$a'~/b", "schemas", "$.components.schemas['$a\\'~/b']"}, + {13, "$bad", "responses", "$.components.responses['$bad']"}, + } { + failure := byLine[want.line] + require.NotNil(t, failure, "missing error at line %d", want.line) + assert.Equal(t, 5, failure.Column) + assert.Equal(t, want.name, failure.FieldName) + assert.Equal(t, want.path, failure.FieldPath) + assert.Equal(t, []string{"components", want.section, want.name}, failure.InstancePath) + assert.Contains(t, failure.Reason, want.name) + assert.Contains(t, failure.Reason, "does not match pattern") + assert.Equal(t, want.name, failure.ReferenceObject) + assert.NotNil(t, failure.OriginalJsonSchemaError) + } + }) + } +} + +func TestDocumentPropertyNamesDoNotReplaceValueErrors(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(`openapi: 3.1.0 +info: + title: Mixed errors + version: 42 +components: + schemas: + $bad: + type: object +`)) + require.NoError(t, err) + valid, failures := ValidateOpenAPIDocument(doc) + require.False(t, valid) + require.Len(t, failures, 1) + var nameError, valueError bool + for _, failure := range failures[0].SchemaValidationErrors { + if failure.FieldName == "$bad" { + nameError = true + assert.Equal(t, 7, failure.Line) + } + if failure.FieldPath == "$.info.version" { + valueError = true + assert.NotContains(t, failure.Reason, "$bad") + assert.Equal(t, 4, failure.Line) + } + } + assert.True(t, nameError) + assert.True(t, valueError) +} + +func TestDocumentPropertyNamesPrecompiledRootConstraint(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(`openapi: 3.1.0 +info: {title: Test, version: '1'} +x: true +`)) + require.NoError(t, err) + compiler := jsonschema.NewCompiler() + require.NoError(t, compiler.AddResource("root.json", map[string]any{ + "propertyNames": map[string]any{"minLength": 2}, + "dependentRequired": map[string]any{"x": []any{"missing"}}, + })) + schema, err := compiler.Compile("root.json") + require.NoError(t, err) + valid, failures := ValidateOpenAPIDocumentWithPrecompiled(doc, schema) + require.False(t, valid) + require.Len(t, failures, 1) + require.Len(t, failures[0].SchemaValidationErrors, 2) + failure := failures[0].SchemaValidationErrors[0] + assert.Equal(t, "x", failure.FieldName) + assert.Equal(t, "$.x", failure.FieldPath) + assert.Equal(t, []string{"x"}, failure.InstancePath) + assert.Equal(t, 3, failure.Line) + assert.Equal(t, 1, failure.Column) + assert.Contains(t, failure.Reason, "minLength") + assert.Contains(t, failures[0].SchemaValidationErrors[1].Reason, "missing") + assert.Equal(t, helpers.Schema, failures[0].ValidationType) +} + +func TestDocumentPropertyNamesKeepFollowingArrayErrors(t *testing.T) { + for _, rule := range []any{false, map[string]any{"minLength": 2}} { + t.Run(fmt.Sprint(rule), func(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(`openapi: 3.1.0 +info: {title: Test, version: '1'} +x-items: + - x: true + - 42 +`)) + require.NoError(t, err) + compiler := jsonschema.NewCompiler() + require.NoError(t, compiler.AddResource("array.json", map[string]any{ + "properties": map[string]any{"x-items": map[string]any{ + "items": map[string]any{"type": "object", "propertyNames": rule}, + }}, + })) + schema, err := compiler.Compile("array.json") + require.NoError(t, err) + valid, failures := ValidateOpenAPIDocumentWithPrecompiled(doc, schema) + require.False(t, valid) + require.Len(t, failures, 1) + require.Len(t, failures[0].SchemaValidationErrors, 2) + name, value := failures[0].SchemaValidationErrors[0], failures[0].SchemaValidationErrors[1] + assert.Equal(t, "$['x-items'][0].x", name.FieldPath) + assert.Equal(t, 4, name.Line) + assert.Equal(t, 5, name.Column) + assert.Equal(t, "$['x-items'][1]", value.FieldPath) + assert.Contains(t, value.Reason, "want object") + }) + } +} diff --git a/schema_validation/property_locator.go b/schema_validation/property_locator.go index 539a4447..3b553d7e 100644 --- a/schema_validation/property_locator.go +++ b/schema_validation/property_locator.go @@ -1,4 +1,4 @@ -// Copyright 2025 Princess B33f Heavy Industries / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation @@ -8,7 +8,7 @@ import ( "strings" "github.com/pb33f/go-yaml" - "github.com/santhosh-tekuri/jsonschema/v6" + "github.com/pb33f/jsonschema/v6" liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/helpers" diff --git a/schema_validation/schema_resources.go b/schema_validation/schema_resources.go index a6833e9a..3c967049 100644 --- a/schema_validation/schema_resources.go +++ b/schema_validation/schema_resources.go @@ -11,11 +11,11 @@ import ( "strings" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/utils" - "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/cache" "github.com/pb33f/libopenapi-validator/config" diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index dff8fde8..0787953c 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -12,8 +12,9 @@ import ( "strings" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" + "github.com/pb33f/jsonschema/v6/kind" "github.com/pb33f/libopenapi" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/language" "golang.org/x/text/message" @@ -341,14 +342,23 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch // flatten the validationErrors schFlatErrs := jk.BasicOutput().Errors - // Extract property name info once before processing errors (performance optimization) - propertyInfo := extractPropertyNameFromError(jk) + propertyErrors := documentPropertyNameErrors(jk) + var propertySchemaLocation string for q := range schFlatErrs { er := schFlatErrs[q] + propertyName, isPropertyName := er.Error.Kind.(*kind.PropertyNames) + if !isPropertyName && propertySchemaLocation != "" && (er.KeywordLocation == propertySchemaLocation || strings.HasPrefix(er.KeywordLocation, propertySchemaLocation+"/")) { + continue // The parent diagnostic describes this key's constraint failure. + } + propertySchemaLocation = "" + if isPropertyName { + // Remove the error kind's keyword, retaining the propertyNames schema location. + propertySchemaLocation = strings.TrimSuffix(er.KeywordLocation, "/propertyNames") + } errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) - if er.KeywordLocation == "" || helpers.IgnorePolyRegex.MatchString(errMsg) { + if er.KeywordLocation == "" || helpers.IgnoreRegex.MatchString(errMsg) { continue // ignore this error, it's useless tbh, utter noise. } if errMsg != "" { @@ -363,6 +373,11 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch KeywordLocation: er.KeywordLocation, OriginalJsonSchemaError: jk, } + if isPropertyName { + applyDocumentPropertyNameError(info.RootNode.Content[0], propertyErrors[propertyName], propertyName.Property, violation) + schemaValidationErrors = append(schemaValidationErrors, violation) + continue + } // if we have a location within the schema, add it to the error if located != nil { @@ -383,9 +398,6 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch if source, err := yaml.Marshal(cloneYAMLNode(located)); err == nil { violation.ReferenceObject = strings.TrimSpace(string(source)) } - } else { - // handles property name validation errors that don't provide useful InstanceLocation - applyPropertyNameFallback(propertyInfo, info.RootNode.Content[0], violation) } schemaValidationErrors = append(schemaValidationErrors, violation) } diff --git a/schema_validation/validate_document_test.go b/schema_validation/validate_document_test.go index bf2e77f6..6440c91a 100644 --- a/schema_validation/validate_document_test.go +++ b/schema_validation/validate_document_test.go @@ -53,7 +53,13 @@ func TestValidateDocument_Invalid31(t *testing.T) { assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 6) + assert.Len(t, errors[0].SchemaValidationErrors, 4) + var fields []string + for _, failure := range errors[0].SchemaValidationErrors { + fields = append(fields, failure.FieldPath) + assert.NotEqual(t, "validation failed", failure.Reason) + } + assert.ElementsMatch(t, []string{"$.info.contact.invalid", "$.info.license", "$.info.license.invalid", "$.info.title"}, fields) } func TestValidateDocument_UnquotedIntegerResponseCodeHelpfulError(t *testing.T) { @@ -449,7 +455,10 @@ info: assert.False(t, valid) assert.Len(t, errors, 1) - assert.Len(t, errors[0].SchemaValidationErrors, 6) + assert.Len(t, errors[0].SchemaValidationErrors, 5) + for _, failure := range errors[0].SchemaValidationErrors { + assert.NotEqual(t, "validation failed", failure.Reason) + } } func TestValidateDocument_NilSpecJSON(t *testing.T) { @@ -523,13 +532,13 @@ func TestValidateDocument_WithPrecompiledSchema_Invalid(t *testing.T) { valid, errs := ValidateOpenAPIDocumentWithPrecompiled(doc, compiledSchema) assert.False(t, valid) assert.Len(t, errs, 1) - assert.Len(t, errs[0].SchemaValidationErrors, 6) + assert.Len(t, errs[0].SchemaValidationErrors, 4) // 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) + assert.Len(t, errs2[0].SchemaValidationErrors, 4) } func TestValidateDocument_SpecJSONBytesPath(t *testing.T) { diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 1f4afb4c..518d9f67 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -15,8 +15,8 @@ import ( "strconv" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/language" "golang.org/x/text/message" diff --git a/schema_validation/validate_schema_extract_errors_test.go b/schema_validation/validate_schema_extract_errors_test.go index 8885ec70..4b44cecf 100644 --- a/schema_validation/validate_schema_extract_errors_test.go +++ b/schema_validation/validate_schema_extract_errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package schema_validation @@ -7,9 +7,9 @@ import ( "testing" "github.com/pb33f/go-yaml" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/testify/assert" "github.com/pb33f/testify/require" - "github.com/santhosh-tekuri/jsonschema/v6" "golang.org/x/text/message" ) diff --git a/strict/matcher.go b/strict/matcher.go index 89bb2c3b..4a34ee77 100644 --- a/strict/matcher.go +++ b/strict/matcher.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley // SPDX-License-Identifier: MIT package strict @@ -7,9 +7,9 @@ import ( "errors" "fmt" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/utils" - "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/helpers" ) diff --git a/strict/types.go b/strict/types.go index 3d11222d..25b02a21 100644 --- a/strict/types.go +++ b/strict/types.go @@ -33,8 +33,8 @@ import ( "log/slog" "regexp" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" - "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi-validator/config" ) diff --git a/validator_test.go b/validator_test.go index b177ba8b..6b1602a9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -18,10 +18,10 @@ import ( "unicode" "github.com/dlclark/regexp2" + "github.com/pb33f/jsonschema/v6" "github.com/pb33f/libopenapi" "github.com/pb33f/testify/assert" "github.com/pb33f/testify/require" - "github.com/santhosh-tekuri/jsonschema/v6" "github.com/pb33f/libopenapi/datamodel/high/base" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" From 806500fd070a6f8ee9bf9df1645bbbee22d77086 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Tue, 6 Oct 2026 10:23:43 -0400 Subject: [PATCH 2/2] fix: handle nested property-name content diagnostics --- schema_validation/document_property_names.go | 3 ++ .../document_property_names_test.go | 43 +++++++++++++++++++ schema_validation/validate_document.go | 3 ++ 3 files changed, 49 insertions(+) diff --git a/schema_validation/document_property_names.go b/schema_validation/document_property_names.go index 0f0abc40..e8b2792c 100644 --- a/schema_validation/document_property_names.go +++ b/schema_validation/document_property_names.go @@ -34,6 +34,9 @@ func documentPropertyNameErrors(root *jsonschema.ValidationError) map[*kind.Prop } func documentPropertyNamePattern(err *jsonschema.ValidationError) string { + if _, ok := err.ErrorKind.(*kind.ContentSchema); ok { + return "" // Patterns in decoded content do not constrain the property name itself. + } if pattern, ok := err.ErrorKind.(*kind.Pattern); ok { return pattern.Want } diff --git a/schema_validation/document_property_names_test.go b/schema_validation/document_property_names_test.go index 4e6b9fb6..219296c5 100644 --- a/schema_validation/document_property_names_test.go +++ b/schema_validation/document_property_names_test.go @@ -159,3 +159,46 @@ x-items: }) } } + +func TestDocumentPropertyNamesWithNestedContentConstraints(t *testing.T) { + for _, rule := range []any{map[string]any{"minLength": 2}, map[string]any{"pattern": "^valid$"}} { + t.Run(fmt.Sprint(rule), func(t *testing.T) { + doc, err := libopenapi.NewDocument([]byte(`openapi: 3.1.0 +info: {title: Test, version: '1'} +x-items: + - '{"x":true}': true + - 42 +`)) + require.NoError(t, err) + compiler := jsonschema.NewCompiler() + compiler.AssertContent() + require.NoError(t, compiler.AddResource("nested.json", map[string]any{ + "properties": map[string]any{"x-items": map[string]any{ + "items": map[string]any{ + "type": "object", + "propertyNames": map[string]any{ + "contentMediaType": "application/json", + "contentSchema": map[string]any{"propertyNames": rule}, + }, + }, + }}, + })) + schema, err := compiler.Compile("nested.json") + require.NoError(t, err) + valid, failures := ValidateOpenAPIDocumentWithPrecompiled(doc, schema) + require.False(t, valid) + require.Len(t, failures, 1) + require.Len(t, failures[0].SchemaValidationErrors, 2) + name, value := failures[0].SchemaValidationErrors[0], failures[0].SchemaValidationErrors[1] + assert.Equal(t, `{"x":true}`, name.FieldName) + assert.Equal(t, []string{"x-items", "0", `{"x":true}`}, name.InstancePath) + assert.Equal(t, `$['x-items'][0]['{"x":true}']`, name.FieldPath) + assert.Equal(t, 4, name.Line) + assert.Equal(t, 5, name.Column) + assert.Contains(t, name.Reason, "contentSchema") + assert.Contains(t, name.Reason, "'x'") + assert.Equal(t, "$['x-items'][1]", value.FieldPath) + assert.Contains(t, value.Reason, "want object") + }) + } +} diff --git a/schema_validation/validate_document.go b/schema_validation/validate_document.go index 0787953c..0b50deeb 100644 --- a/schema_validation/validate_document.go +++ b/schema_validation/validate_document.go @@ -348,6 +348,9 @@ func ValidateOpenAPIDocumentWithPrecompiled(doc libopenapi.Document, compiledSch for q := range schFlatErrs { er := schFlatErrs[q] propertyName, isPropertyName := er.Error.Kind.(*kind.PropertyNames) + if isPropertyName && propertyErrors[propertyName] == nil { + continue // Nested key constraints belong to the outer property-name diagnostic. + } if !isPropertyName && propertySchemaLocation != "" && (er.KeywordLocation == propertySchemaLocation || strings.HasPrefix(er.KeywordLocation, propertySchemaLocation+"/")) { continue // The parent diagnostic describes this key's constraint failure. }