Skip to content

Commit efc5ceb

Browse files
luke-hagar-spdaveshanley
authored andcommitted
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.
1 parent cdbc6a9 commit efc5ceb

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

index/extract_refs.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ func isArrayOfSchemaContainingNode(v string) bool {
5757
return false
5858
}
5959

60+
// underOpenAPIExamplePath reports whether seenPath is under OpenAPI example or examples payload
61+
// (sample data, not schema). Matches description/summary and properties skipping in this file.
62+
func underOpenAPIExamplePath(seenPath []string) bool {
63+
return slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples")
64+
}
65+
6066
// ExtractRefs will return a deduplicated slice of references for every unique ref found in the document.
6167
// The total number of refs, will generally be much higher, you can extract those from GetRawReferenceCount()
6268
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
7783

7884
// Check if THIS node has a $id and update scope for processing children
7985
// This must happen before iterating children so they see the updated scope
80-
if node.Kind == yaml.MappingNode {
86+
if node.Kind == yaml.MappingNode && !underOpenAPIExamplePath(seenPath) {
8187
if nodeId := FindSchemaIdInNode(node); nodeId != "" {
8288
resolvedNodeId, _ := ResolveSchemaId(nodeId, parentBaseUri)
8389
if resolvedNodeId == "" {
@@ -557,6 +563,9 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
557563

558564
// Detect and register JSON Schema 2020-12 $id declarations
559565
if i%2 == 0 && n.Value == "$id" {
566+
if underOpenAPIExamplePath(seenPath) {
567+
continue
568+
}
560569
if len(node.Content) > i+1 && utils.IsNodeStringValue(node.Content[i+1]) {
561570
idValue := node.Content[i+1].Value
562571
idNode := node.Content[i+1]

index/schema_id_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,118 @@ components:
750750
assert.Contains(t, petEntry.DefinitionPath, "Pet")
751751
}
752752

753+
func TestSchemaId_IgnoredUnderExampleAndExamples(t *testing.T) {
754+
t.Run("example_payload", func(t *testing.T) {
755+
spec := `openapi: "3.1.0"
756+
info:
757+
title: Test API
758+
version: 1.0.0
759+
paths:
760+
/pets:
761+
get:
762+
responses:
763+
"200":
764+
description: ok
765+
content:
766+
application/json:
767+
schema:
768+
$id: "https://example.com/schemas/pet.json"
769+
type: object
770+
properties:
771+
id:
772+
type: string
773+
example:
774+
$id: "https://example.com/should-not-register"
775+
id: "1"
776+
`
777+
var rootNode yaml.Node
778+
err := yaml.Unmarshal([]byte(spec), &rootNode)
779+
assert.NoError(t, err)
780+
781+
config := CreateClosedAPIIndexConfig()
782+
config.SpecAbsolutePath = "https://example.com/openapi.yaml"
783+
index := NewSpecIndexWithConfig(&rootNode, config)
784+
assert.NotNil(t, index)
785+
786+
allIds := index.GetAllSchemaIds()
787+
assert.Len(t, allIds, 1)
788+
assert.NotNil(t, allIds["https://example.com/schemas/pet.json"])
789+
assert.Nil(t, allIds["https://example.com/should-not-register"])
790+
})
791+
792+
t.Run("examples_named_value", func(t *testing.T) {
793+
spec := `openapi: "3.1.0"
794+
info:
795+
title: Test API
796+
version: 1.0.0
797+
paths:
798+
/widgets:
799+
get:
800+
responses:
801+
"200":
802+
description: ok
803+
content:
804+
application/json:
805+
schema:
806+
$id: "https://example.com/schemas/widget.json"
807+
type: object
808+
examples:
809+
sample:
810+
value:
811+
$id: "https://example.com/fake-from-examples"
812+
foo: bar
813+
`
814+
var rootNode yaml.Node
815+
err := yaml.Unmarshal([]byte(spec), &rootNode)
816+
assert.NoError(t, err)
817+
818+
config := CreateClosedAPIIndexConfig()
819+
config.SpecAbsolutePath = "https://example.com/openapi.yaml"
820+
index := NewSpecIndexWithConfig(&rootNode, config)
821+
assert.NotNil(t, index)
822+
823+
allIds := index.GetAllSchemaIds()
824+
assert.Len(t, allIds, 1)
825+
assert.NotNil(t, allIds["https://example.com/schemas/widget.json"])
826+
assert.Nil(t, allIds["https://example.com/fake-from-examples"])
827+
})
828+
829+
t.Run("invalid_id_in_example_no_index_error", func(t *testing.T) {
830+
spec := `openapi: "3.1.0"
831+
info:
832+
title: Test API
833+
version: 1.0.0
834+
paths:
835+
/x:
836+
get:
837+
responses:
838+
"200":
839+
description: ok
840+
content:
841+
application/json:
842+
schema:
843+
type: object
844+
example:
845+
$id: "https://bad.com/schema#fragment"
846+
k: v
847+
`
848+
var rootNode yaml.Node
849+
err := yaml.Unmarshal([]byte(spec), &rootNode)
850+
assert.NoError(t, err)
851+
852+
config := CreateClosedAPIIndexConfig()
853+
config.SpecAbsolutePath = "https://example.com/openapi.yaml"
854+
index := NewSpecIndexWithConfig(&rootNode, config)
855+
assert.NotNil(t, index)
856+
857+
assert.Len(t, index.GetAllSchemaIds(), 0)
858+
for _, e := range index.GetReferenceIndexErrors() {
859+
assert.False(t, strings.Contains(e.Error(), "invalid $id"),
860+
"$id inside example must not be validated as schema $id: %v", e)
861+
}
862+
})
863+
}
864+
753865
func TestSchemaId_ExtractionWithInvalidId(t *testing.T) {
754866
// OpenAPI 3.1 spec with invalid $id (contains fragment)
755867
spec := `openapi: "3.1.0"

0 commit comments

Comments
 (0)