From 195dcb00028672474719226baed33e4fb444a53f Mon Sep 17 00:00:00 2001 From: Luke Hagar Date: Tue, 24 Mar 2026 12:53:40 -0500 Subject: [PATCH 1/3] Add underOpenAPIExamplePath function to filter schema IDs in examples - Introduced underOpenAPIExamplePath function to determine if a path is under OpenAPI example or examples payload. - Updated ExtractRefs method to skip processing nodes under example paths. - Added tests to ensure schema IDs in examples are not registered, confirming correct behavior for both example and examples payloads. --- index/extract_refs.go | 11 +++- index/schema_id_test.go | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/index/extract_refs.go b/index/extract_refs.go index 176fe51c..f295445b 100644 --- a/index/extract_refs.go +++ b/index/extract_refs.go @@ -57,6 +57,12 @@ func isArrayOfSchemaContainingNode(v string) bool { return false } +// underOpenAPIExamplePath reports whether seenPath is under OpenAPI example or examples payload +// (sample data, not schema). Matches description/summary and properties skipping in this file. +func underOpenAPIExamplePath(seenPath []string) bool { + return slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples") +} + // ExtractRefs will return a deduplicated slice of references for every unique ref found in the document. // The total number of refs, will generally be much higher, you can extract those from GetRawReferenceCount() func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node, seenPath []string, level int, poly bool, pName string) []*Reference { @@ -77,7 +83,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node // Check if THIS node has a $id and update scope for processing children // This must happen before iterating children so they see the updated scope - if node.Kind == yaml.MappingNode { + if node.Kind == yaml.MappingNode && !underOpenAPIExamplePath(seenPath) { if nodeId := FindSchemaIdInNode(node); nodeId != "" { resolvedNodeId, _ := ResolveSchemaId(nodeId, parentBaseUri) if resolvedNodeId == "" { @@ -557,6 +563,9 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node // Detect and register JSON Schema 2020-12 $id declarations if i%2 == 0 && n.Value == "$id" { + if underOpenAPIExamplePath(seenPath) { + continue + } if len(node.Content) > i+1 && utils.IsNodeStringValue(node.Content[i+1]) { idValue := node.Content[i+1].Value idNode := node.Content[i+1] diff --git a/index/schema_id_test.go b/index/schema_id_test.go index feed0d80..d9f1cfee 100644 --- a/index/schema_id_test.go +++ b/index/schema_id_test.go @@ -750,6 +750,118 @@ components: assert.Contains(t, petEntry.DefinitionPath, "Pet") } +func TestSchemaId_IgnoredUnderExampleAndExamples(t *testing.T) { + t.Run("example_payload", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +paths: + /pets: + get: + responses: + "200": + description: ok + content: + application/json: + schema: + $id: "https://example.com/schemas/pet.json" + type: object + properties: + id: + type: string + example: + $id: "https://example.com/should-not-register" + id: "1" +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + allIds := index.GetAllSchemaIds() + assert.Len(t, allIds, 1) + assert.NotNil(t, allIds["https://example.com/schemas/pet.json"]) + assert.Nil(t, allIds["https://example.com/should-not-register"]) + }) + + t.Run("examples_named_value", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +paths: + /widgets: + get: + responses: + "200": + description: ok + content: + application/json: + schema: + $id: "https://example.com/schemas/widget.json" + type: object + examples: + sample: + value: + $id: "https://example.com/fake-from-examples" + foo: bar +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + allIds := index.GetAllSchemaIds() + assert.Len(t, allIds, 1) + assert.NotNil(t, allIds["https://example.com/schemas/widget.json"]) + assert.Nil(t, allIds["https://example.com/fake-from-examples"]) + }) + + t.Run("invalid_id_in_example_no_index_error", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +paths: + /x: + get: + responses: + "200": + description: ok + content: + application/json: + schema: + type: object + example: + $id: "https://bad.com/schema#fragment" + k: v +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + assert.Len(t, index.GetAllSchemaIds(), 0) + for _, e := range index.GetReferenceIndexErrors() { + assert.False(t, strings.Contains(e.Error(), "invalid $id"), + "$id inside example must not be validated as schema $id: %v", e) + } + }) +} + func TestSchemaId_ExtractionWithInvalidId(t *testing.T) { // OpenAPI 3.1 spec with invalid $id (contains fragment) spec := `openapi: "3.1.0" From 8ad13539b69b3bdcc53b1f45b1b5369d6aba386a Mon Sep 17 00:00:00 2001 From: Luke Hagar Date: Tue, 24 Mar 2026 13:09:36 -0500 Subject: [PATCH 2/3] Add unit tests for underOpenAPIExamplePath function - Introduced a new test function, TestUnderOpenAPIExamplePath, to validate the behavior of underOpenAPIExamplePath. - Added various test cases to check for correct identification of paths under OpenAPI example and examples segments. - Ensured comprehensive coverage for edge cases, including empty paths and paths not fully under example segments. --- index/extract_refs_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/index/extract_refs_test.go b/index/extract_refs_test.go index dcf89edf..f6b4cf14 100644 --- a/index/extract_refs_test.go +++ b/index/extract_refs_test.go @@ -711,3 +711,22 @@ components: func TestSpecIndex_isExternalReference_Nil(t *testing.T) { assert.False(t, isExternalReference(nil)) } + +func TestUnderOpenAPIExamplePath(t *testing.T) { + tests := []struct { + name string + path []string + want bool + }{ + {"empty", nil, false}, + {"no_example_segments", []string{"paths", "get", "responses", "200", "content", "application/json", "schema"}, false}, + {"under_example", []string{"paths", "get", "responses", "200", "content", "application/json", "schema", "example"}, true}, + {"under_examples", []string{"content", "application/json", "schema", "examples", "sample", "value"}, true}, + {"example_not_whole_segment", []string{"paths", "exampled"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, underOpenAPIExamplePath(tt.path)) + }) + } +} From 47d444688e2f07f15d733b41815a3aca186cff84 Mon Sep 17 00:00:00 2001 From: Luke Hagar Date: Mon, 30 Mar 2026 10:14:52 -0500 Subject: [PATCH 3/3] Fix underOpenAPIExamplePath to distinguish property names from OpenAPI keywords MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation matched any path segment named "example" or "examples", which incorrectly suppressed $id extraction for schemas under properties with those names. Now checks the preceding segment — if it is "properties" or "patternProperties", the segment is a property name and not an OpenAPI example keyword. Co-Authored-By: Claude Opus 4.6 (1M context) --- index/extract_refs.go | 29 ++++++---- index/extract_refs_test.go | 5 ++ index/schema_id_test.go | 106 +++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/index/extract_refs.go b/index/extract_refs.go index f295445b..154e0280 100644 --- a/index/extract_refs.go +++ b/index/extract_refs.go @@ -11,7 +11,6 @@ import ( "os" "path/filepath" "runtime" - "slices" "sort" "strconv" "strings" @@ -57,10 +56,18 @@ func isArrayOfSchemaContainingNode(v string) bool { return false } -// underOpenAPIExamplePath reports whether seenPath is under OpenAPI example or examples payload -// (sample data, not schema). Matches description/summary and properties skipping in this file. +// underOpenAPIExamplePath reports whether seenPath is under an OpenAPI example or examples +// keyword (sample data, not schema). A segment named "example" or "examples" that is preceded +// by "properties" or "patternProperties" is a schema property name, not an OpenAPI keyword. func underOpenAPIExamplePath(seenPath []string) bool { - return slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples") + for i, p := range seenPath { + if p == "example" || p == "examples" { + if i == 0 || (seenPath[i-1] != "properties" && seenPath[i-1] != "patternProperties") { + return true + } + } + } + return false } // ExtractRefs will return a deduplicated slice of references for every unique ref found in the document. @@ -176,11 +183,13 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node if len(seenPath) > 0 { skip := false - // iterate through the path and look for an item named 'examples' or 'example' - for _, p := range seenPath { + // iterate through the path and look for an OpenAPI example/examples keyword or extension + for j, p := range seenPath { if p == "examples" || p == "example" { - skip = true - break + if j == 0 || (seenPath[j-1] != "properties" && seenPath[j-1] != "patternProperties") { + skip = true + break + } } // look for any extension in the path and ignore it if strings.HasPrefix(p, "x-") { @@ -670,7 +679,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node prev = n.Value continue } - if !slices.Contains(seenPath, "example") && !slices.Contains(seenPath, "examples") { + if !underOpenAPIExamplePath(seenPath) { ref := &DescriptionReference{ ParentNode: parent, Content: node.Content[i+1].Value, @@ -699,7 +708,7 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node continue } - if slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples") { + if underOpenAPIExamplePath(seenPath) { continue } diff --git a/index/extract_refs_test.go b/index/extract_refs_test.go index f6b4cf14..44db8447 100644 --- a/index/extract_refs_test.go +++ b/index/extract_refs_test.go @@ -723,6 +723,11 @@ func TestUnderOpenAPIExamplePath(t *testing.T) { {"under_example", []string{"paths", "get", "responses", "200", "content", "application/json", "schema", "example"}, true}, {"under_examples", []string{"content", "application/json", "schema", "examples", "sample", "value"}, true}, {"example_not_whole_segment", []string{"paths", "exampled"}, false}, + {"example_as_property_name", []string{"components", "schemas", "Foo", "properties", "example"}, false}, + {"examples_as_property_name", []string{"components", "schemas", "Foo", "properties", "examples"}, false}, + {"nested_under_property_example", []string{"components", "schemas", "Foo", "properties", "example", "properties", "id"}, false}, + {"patternProperties_example", []string{"components", "schemas", "Foo", "patternProperties", "example"}, false}, + {"real_example_after_property_example", []string{"components", "schemas", "Foo", "properties", "example", "example"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/index/schema_id_test.go b/index/schema_id_test.go index d9f1cfee..af0050e6 100644 --- a/index/schema_id_test.go +++ b/index/schema_id_test.go @@ -862,6 +862,112 @@ paths: }) } +func TestSchemaId_NotIgnoredUnderPropertiesExample(t *testing.T) { + t.Run("property_named_example", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +components: + schemas: + MySchema: + $id: "https://example.com/schemas/myschema.json" + type: object + properties: + example: + $id: "https://example.com/schemas/example-prop.json" + type: object + properties: + id: + type: string +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + allIds := index.GetAllSchemaIds() + assert.Len(t, allIds, 2) + assert.NotNil(t, allIds["https://example.com/schemas/myschema.json"]) + assert.NotNil(t, allIds["https://example.com/schemas/example-prop.json"]) + }) + + t.Run("property_named_examples", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +components: + schemas: + MySchema: + type: object + properties: + examples: + $id: "https://example.com/schemas/examples-prop.json" + type: object + properties: + list: + type: array +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + allIds := index.GetAllSchemaIds() + assert.Len(t, allIds, 1) + assert.NotNil(t, allIds["https://example.com/schemas/examples-prop.json"]) + }) + + t.Run("real_example_still_ignored", func(t *testing.T) { + spec := `openapi: "3.1.0" +info: + title: Test API + version: 1.0.0 +paths: + /pets: + get: + responses: + "200": + description: ok + content: + application/json: + schema: + $id: "https://example.com/schemas/pet.json" + type: object + properties: + example: + $id: "https://example.com/schemas/example-prop.json" + type: string + example: + $id: "https://example.com/should-not-register" + id: "1" +` + var rootNode yaml.Node + err := yaml.Unmarshal([]byte(spec), &rootNode) + assert.NoError(t, err) + + config := CreateClosedAPIIndexConfig() + config.SpecAbsolutePath = "https://example.com/openapi.yaml" + index := NewSpecIndexWithConfig(&rootNode, config) + assert.NotNil(t, index) + + allIds := index.GetAllSchemaIds() + assert.Len(t, allIds, 2) + assert.NotNil(t, allIds["https://example.com/schemas/pet.json"]) + assert.NotNil(t, allIds["https://example.com/schemas/example-prop.json"]) + assert.Nil(t, allIds["https://example.com/should-not-register"]) + }) +} + func TestSchemaId_ExtractionWithInvalidId(t *testing.T) { // OpenAPI 3.1 spec with invalid $id (contains fragment) spec := `openapi: "3.1.0"