@@ -145,20 +145,29 @@ func (os OASSchema) RunRule(nodes []*yaml.Node, context model.RuleFunctionContex
145145 }
146146
147147 schemaErr := validationErrors [i ].SchemaValidationErrors [y ]
148- var reason = schemaErr .Reason
149- // if reason is empty or generic, extract leaf error messages (issue #766)
150- if reason == "validation failed" || reason == "" {
151- if schemaErr .OriginalError != nil {
152- leafErrors := extractLeafValidationErrors (schemaErr .OriginalError )
153- if len (leafErrors ) > 0 {
154- reason = strings .Join (leafErrors , "; " )
148+ var reason string
149+
150+ // Always prefer leaf error extraction from OriginalError when available (issue #766)
151+ if schemaErr .OriginalError != nil {
152+ leafErrors := extractLeafValidationErrors (schemaErr .OriginalError )
153+ if len (leafErrors ) > 0 {
154+ // Limit to last 3 leaf errors for readability
155+ if len (leafErrors ) > 3 {
156+ leafErrors = leafErrors [len (leafErrors )- 3 :]
155157 }
156- }
157- // fallback to generic message
158- if reason == "" {
159- reason = "multiple components failed validation"
158+ reason = strings .Join (leafErrors , "; " )
160159 }
161160 }
161+
162+ // Fallback to Reason if no leaf errors extracted
163+ if reason == "" {
164+ reason = schemaErr .Reason
165+ }
166+
167+ // Final fallback
168+ if reason == "" {
169+ reason = "schema validation failed"
170+ }
162171 res := model.RuleFunctionResult {
163172 Message : fmt .Sprintf ("schema invalid: %v" , reason ),
164173 StartNode : n ,
@@ -214,10 +223,13 @@ func checkForNullableKeyword(context model.RuleFunctionContext) []model.RuleFunc
214223 return results
215224}
216225
217- // extractLeafValidationErrors extracts only leaf error messages from a jsonschema.ValidationError tree (issue #766)
226+ // extractLeafValidationErrors extracts only meaningful leaf error messages from a jsonschema.ValidationError tree (issue #766)
227+ // It filters out noise from oneOf/anyOf schema structures (like "missing properties: [$ref]")
218228func extractLeafValidationErrors (err * jsonschema.ValidationError ) []string {
219- var results []string
229+ var highPriority []string // Type errors, pattern errors, format errors - the real issues
230+ var lowPriority []string // Enum errors (often noise from oneOf branches)
220231 seen := make (map [string ]bool )
232+ seenPaths := make (map [string ]bool ) // Track paths we've already reported errors for
221233 const maxDepth = 50
222234
223235 var extract func (e * jsonschema.ValidationError , depth int )
@@ -229,12 +241,26 @@ func extractLeafValidationErrors(err *jsonschema.ValidationError) []string {
229241 // if this is a leaf node (no causes), extract the message
230242 if len (e .Causes ) == 0 {
231243 path := "/" + strings .Join (e .InstanceLocation , "/" )
244+
245+ // Skip noise errors from oneOf/anyOf schema structures
246+ if isNoiseError (e .ErrorKind ) {
247+ return
248+ }
249+
232250 msg := errorKindToString (e .ErrorKind )
233251 if msg != "" {
234252 fullMsg := fmt .Sprintf ("`%s` %s" , path , msg )
235253 if ! seen [fullMsg ] {
236254 seen [fullMsg ] = true
237- results = append (results , fullMsg )
255+
256+ // Categorize by priority
257+ if isHighPriorityError (e .ErrorKind ) {
258+ highPriority = append (highPriority , fullMsg )
259+ seenPaths [path ] = true
260+ } else if ! seenPaths [path ] {
261+ // Only add low priority if we haven't seen this path in high priority
262+ lowPriority = append (lowPriority , fullMsg )
263+ }
238264 }
239265 }
240266 }
@@ -246,7 +272,54 @@ func extractLeafValidationErrors(err *jsonschema.ValidationError) []string {
246272 }
247273
248274 extract (err , 0 )
249- return results
275+
276+ // Return high priority errors first, then low priority
277+ if len (highPriority ) > 0 {
278+ return highPriority
279+ }
280+ return lowPriority
281+ }
282+
283+ // isNoiseError returns true for errors that are noise from oneOf/anyOf schema structures
284+ func isNoiseError (ek jsonschema.ErrorKind ) bool {
285+ if ek == nil {
286+ return true
287+ }
288+ switch k := ek .(type ) {
289+ case * kind.Required :
290+ // "missing properties: [$ref]" is noise from oneOf branches in OpenAPI schemas
291+ for _ , missing := range k .Missing {
292+ if missing == "$ref" {
293+ return true
294+ }
295+ }
296+ case * kind.FalseSchema :
297+ // "property not allowed" from false schema branches
298+ return true
299+ }
300+ return false
301+ }
302+
303+ // isHighPriorityError returns true for errors that represent the actual root cause
304+ func isHighPriorityError (ek jsonschema.ErrorKind ) bool {
305+ if ek == nil {
306+ return false
307+ }
308+ switch ek .(type ) {
309+ case * kind.Type :
310+ return true // Wrong type is usually the real issue
311+ case * kind.Pattern :
312+ return true // Pattern mismatch is specific
313+ case * kind.Format :
314+ return true // Format error is specific
315+ case * kind.Const :
316+ return true // Const mismatch is specific
317+ case * kind.AdditionalProperties :
318+ return true // Extra properties is specific
319+ case * kind.Required :
320+ return true // Missing required field (that isn't $ref)
321+ }
322+ return false
250323}
251324
252325// errorKindToString converts a jsonschema ErrorKind to a human-readable string
0 commit comments