diff --git a/pkg/parser/json_path_locator.go b/pkg/parser/json_path_locator.go index 60d718a57fd..f3c9e560044 100644 --- a/pkg/parser/json_path_locator.go +++ b/pkg/parser/json_path_locator.go @@ -39,6 +39,7 @@ func ExtractJSONPathFromValidationError(err error) []JSONPathInfo { Message: cause.Error(), Location: cause.InstanceLocation, ErrorKind: cause.ErrorKind, + Causes: cause.Causes, } paths = append(paths, path) } @@ -49,10 +50,11 @@ func ExtractJSONPathFromValidationError(err error) []JSONPathInfo { // JSONPathInfo holds information about a validation error and its path type JSONPathInfo struct { - Path string // JSON path like "/tools/1" or "/age" - Message string // Error message - Location []string // Instance location from jsonschema (e.g., ["tools", "1"]) - ErrorKind jsonschema.ErrorKind // Structural error kind; nil when built from a string-only context + Path string // JSON path like "/tools/1" or "/age" + Message string // Error message + Location []string // Instance location from jsonschema (e.g., ["tools", "1"]) + ErrorKind jsonschema.ErrorKind // Structural error kind; nil when built from a string-only context + Causes []*jsonschema.ValidationError // Nested validation errors from a composite error kind such as OneOf or Group } // convertInstanceLocationToJSONPath converts jsonschema InstanceLocation to JSON path string @@ -105,7 +107,8 @@ func LocateJSONPathForPathInfo(yamlContent string, info JSONPathInfo) JSONPathLo } // additionalPropertyNamesFor returns the disallowed property names for a JSONPathInfo. -// It checks ErrorKind first (structural, no string parsing) and falls back to regex on Message. +// It checks ErrorKind first (structural, no string parsing) and falls back to regex on Message +// only when no structural error kind is available. func additionalPropertyNamesFor(info JSONPathInfo) []string { if info.ErrorKind != nil { if ap, ok := info.ErrorKind.(*kind.AdditionalProperties); ok { @@ -113,9 +116,7 @@ func additionalPropertyNamesFor(info JSONPathInfo) []string { } switch info.ErrorKind.(type) { case *kind.OneOf, *kind.AnyOf, *kind.AllOf, *kind.Group: - // Composite errors may wrap an additional-properties leaf; regex fallback - // preserves the historical location behavior for these aggregate kinds. - return extractAdditionalPropertyNames(info.Message) + return additionalPropertyNamesFromCauses(info.Causes) default: return nil } @@ -123,6 +124,23 @@ func additionalPropertyNamesFor(info JSONPathInfo) []string { return extractAdditionalPropertyNames(info.Message) } +func additionalPropertyNamesFromCauses(causes []*jsonschema.ValidationError) []string { + var names []string + for _, cause := range causes { + if cause == nil { + continue + } + if ap, ok := cause.ErrorKind.(*kind.AdditionalProperties); ok { + names = append(names, ap.Properties...) + continue + } + if len(cause.Causes) > 0 { + names = append(names, additionalPropertyNamesFromCauses(cause.Causes)...) + } + } + return names +} + func findPathInYAMLLines(yamlContent string, pathSegments []PathSegment) JSONPathLocation { lines := strings.Split(yamlContent, "\n") diff --git a/pkg/parser/json_path_locator_improvements_test.go b/pkg/parser/json_path_locator_improvements_test.go index d5ed26f66c8..a1d6bbdd62b 100644 --- a/pkg/parser/json_path_locator_improvements_test.go +++ b/pkg/parser/json_path_locator_improvements_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/santhosh-tekuri/jsonschema/v6" "github.com/santhosh-tekuri/jsonschema/v6/kind" ) @@ -179,7 +180,7 @@ func TestLocateJSONPathForPathInfoSkipsRegexForNonAdditionalPropertiesErrorKind( } } -func TestLocateJSONPathForPathInfoUsesRegexFallbackForGroupErrorKind(t *testing.T) { +func TestLocateJSONPathForPathInfoUsesGroupCausesForAdditionalProperties(t *testing.T) { yamlContent := `on: push: branches: [main] @@ -187,8 +188,11 @@ func TestLocateJSONPathForPathInfoUsesRegexFallbackForGroupErrorKind(t *testing. info := JSONPathInfo{ Path: "/on", - Message: "at '/on': additional properties 'foobar' not allowed", + Message: "this message is intentionally not parseable", ErrorKind: &kind.Group{}, + Causes: []*jsonschema.ValidationError{ + {ErrorKind: &kind.AdditionalProperties{Properties: []string{"foobar"}}}, + }, } location := LocateJSONPathForPathInfo(yamlContent, info) @@ -200,7 +204,26 @@ func TestLocateJSONPathForPathInfoUsesRegexFallbackForGroupErrorKind(t *testing. } } -func TestLocateJSONPathForPathInfoUsesRegexFallbackForOneOfErrorKind(t *testing.T) { +func TestLocateJSONPathForPathInfoDoesNotRegexCompositeErrorKind(t *testing.T) { + yamlContent := `on: + push: + branches: [main] + foobar: invalid` + + info := JSONPathInfo{ + Path: "/on", + Message: "at '/on': additional properties 'foobar' not allowed", + ErrorKind: &kind.Group{}, + } + + location := LocateJSONPathForPathInfo(yamlContent, info) + expected := LocateJSONPathInYAML(yamlContent, "/on") + if location != expected { + t.Fatalf("expected fallback to LocateJSONPathInYAML location %+v, got %+v", expected, location) + } +} + +func TestLocateJSONPathForPathInfoUsesNestedAdditionalPropertiesErrorKind(t *testing.T) { yamlContent := `on: push: branches: [main] @@ -208,8 +231,17 @@ func TestLocateJSONPathForPathInfoUsesRegexFallbackForOneOfErrorKind(t *testing. info := JSONPathInfo{ Path: "/on", - Message: "at '/on': 'oneOf' failed, none matched\n- at '/on': additional properties 'foobar' not allowed\n- at '/on': got object, want null", + Message: "this message is intentionally not parseable", ErrorKind: &kind.OneOf{}, + Causes: []*jsonschema.ValidationError{ + { + ErrorKind: &kind.Group{}, + Causes: []*jsonschema.ValidationError{ + {ErrorKind: &kind.AdditionalProperties{Properties: []string{"foobar"}}}, + }, + }, + {ErrorKind: &kind.Type{}}, + }, } location := LocateJSONPathForPathInfo(yamlContent, info) diff --git a/pkg/parser/schema_compiler.go b/pkg/parser/schema_compiler.go index 60c2332666b..482b8f43007 100644 --- a/pkg/parser/schema_compiler.go +++ b/pkg/parser/schema_compiler.go @@ -112,6 +112,7 @@ func CompileSchema(schemaJSON, schemaURL string) (*jsonschema.Schema, error) { // Create a new compiler compiler := jsonschema.NewCompiler() + compiler.AssertFormat() // Parse the schema JSON first var schemaDoc any diff --git a/pkg/parser/schema_compiler_test.go b/pkg/parser/schema_compiler_test.go index 05a6b67cf93..c3bfd2b68af 100644 --- a/pkg/parser/schema_compiler_test.go +++ b/pkg/parser/schema_compiler_test.go @@ -27,3 +27,26 @@ func TestCompileSchema(t *testing.T) { t.Fatal("Validate(invalid) returned nil error, want validation failure") } } + +func TestCompileSchemaAssertsFormat(t *testing.T) { + t.Parallel() + + schema, err := CompileSchema(`{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "expires": {"type": "string", "format": "date"} + } + }`, "http://example.com/format-schema.json") + if err != nil { + t.Fatalf("CompileSchema returned error: %v", err) + } + + if err := schema.Validate(map[string]any{"expires": "2026-08-28"}); err != nil { + t.Fatalf("Validate(valid date) returned error: %v", err) + } + + if err := schema.Validate(map[string]any{"expires": "not-a-date"}); err == nil { + t.Fatal("Validate(invalid date) returned nil error, want format validation failure") + } +}