From a85d547cee13defdc9b60f6641bca42324fb798a Mon Sep 17 00:00:00 2001 From: quobix Date: Wed, 27 May 2026 14:46:52 -0400 Subject: [PATCH 1/3] correct and deep fix for #575 --- datamodel/high/base/schema.go | 45 +++- datamodel/high/base/schema_proxy.go | 221 +++++++++++++--- datamodel/high/base/schema_proxy_test.go | 241 ++++++++++++++++++ datamodel/high/base/schema_test.go | 14 + .../low/base/schema_build_coverage_test.go | 19 +- datamodel/low/base/schema_build_helpers.go | 8 +- datamodel/low/base/schema_proxy.go | 62 ++++- datamodel/low/base/schema_proxy_test.go | 13 + datamodel/low/base/sibling_ref_transformer.go | 78 ++++-- .../low/base/sibling_ref_transformer_test.go | 25 ++ document_test.go | 112 ++++---- generator/golang/from_openapi.go | 12 + generator/golang/generator_test.go | 41 +++ renderer/mock_render_context.go | 13 + renderer/schema_renderer.go | 5 + renderer/schema_renderer_test.go | 31 +++ 16 files changed, 794 insertions(+), 146 deletions(-) diff --git a/datamodel/high/base/schema.go b/datamodel/high/base/schema.go index f606676ff..f65771057 100644 --- a/datamodel/high/base/schema.go +++ b/datamodel/high/base/schema.go @@ -588,6 +588,14 @@ func (s *Schema) MarshalYAML() (interface{}, error) { // MarshalJSON will create a ready to render JSON representation of the Schema object. func (s *Schema) MarshalJSON() ([]byte, error) { + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + node, err := s.ParentProxy.referenceYAMLNodeForSchema(s) + if err != nil { + return nil, err + } + return marshalYAMLNodeJSON(node) + } + nb := high.NewNodeBuilder(s, s.low) // determine index version @@ -599,13 +607,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) { } // render node node := nb.Render() - var renderedJSON map[string]interface{} - - // marshal into struct - _ = node.Decode(&renderedJSON) - - // return JSON bytes - return json.Marshal(renderedJSON) + return marshalYAMLNodeJSON(node) } // MarshalYAMLInlineWithContext will render out the Schema pointer as YAML using the provided @@ -621,6 +623,9 @@ func (s *Schema) MarshalYAMLInlineWithContext(ctx any) (interface{}, error) { renderCtx = NewInlineRenderContext() ctx = renderCtx } + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + return s.ParentProxy.marshalParsedRefWithSiblingsInline(renderCtx, s) + } // determine if we should preserve discriminator refs based on rendering mode. // in validation mode, we need to fully inline all refs for the JSON schema compiler. @@ -662,6 +667,14 @@ func (s *Schema) MarshalYAMLInline() (interface{}, error) { // MarshalJSONInline will render out the Schema pointer as JSON, and all refs will be inlined fully func (s *Schema) MarshalJSONInline() ([]byte, error) { + if s.ParentProxy != nil && s.ParentProxy.isParsedRefWithSiblings() { + rendered, err := s.MarshalYAMLInline() + if err != nil { + return nil, err + } + return marshalYAMLRenderJSON(rendered) + } + nb := high.NewNodeBuilder(s, s.low) nb.Resolve = true // determine index version @@ -673,11 +686,21 @@ func (s *Schema) MarshalJSONInline() ([]byte, error) { } // render node node := nb.Render() - var renderedJSON map[string]interface{} + return marshalYAMLNodeJSON(node) +} - // marshal into struct - _ = node.Decode(&renderedJSON) +func marshalYAMLRenderJSON(rendered interface{}) ([]byte, error) { + node, ok := yamlNodeFromRender(rendered) + if !ok { + return nil, errors.New("unable to render schema as JSON: YAML render was not a node") + } + return marshalYAMLNodeJSON(node) +} - // return JSON bytes +func marshalYAMLNodeJSON(node *yaml.Node) ([]byte, error) { + var renderedJSON map[string]interface{} + if err := node.Decode(&renderedJSON); err != nil { + return nil, err + } return json.Marshal(renderedJSON) } diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index d9bae1ef0..cee10e4e7 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -243,6 +243,11 @@ func (sp *SchemaProxy) Schema() *Schema { return nil } + if sp.isParsedRefWithSiblings() { + sp.rendered = sp.buildSiblingOnlySchemaView() + return sp.rendered + } + // check the high-level cache first. idx := sp.schema.Value.GetIndex() if idx != nil && sp.schema.Value != nil { @@ -294,6 +299,8 @@ func (sp *SchemaProxy) Schema() *Schema { } // IsReference returns true if the SchemaProxy is a reference to another Schema. +// For parsed OpenAPI 3.1 $ref-with-siblings schemas, the low proxy is backed by +// an internal allOf node, but the high-level API reflects the authored $ref. func (sp *SchemaProxy) IsReference() bool { if sp == nil { return false @@ -302,6 +309,9 @@ func (sp *SchemaProxy) IsReference() bool { if sp.refStr != "" { return true } + if sp.isParsedRefWithSiblings() { + return true + } if sp.schema != nil && sp.schema.Value != nil { return sp.schema.Value.IsReference() } @@ -313,11 +323,17 @@ func (sp *SchemaProxy) GetReference() string { if sp.refStr != "" { return sp.refStr } + if sp.isParsedRefWithSiblings() { + return sp.schema.Value.GetTransformedRefReference() + } if refNode := sp.GetReferenceNode(); refNode != nil { if refValNode := utils.GetRefValueNode(refNode); refValNode != nil { return refValNode.Value } } + if sp.schema == nil || sp.schema.Value == nil { + return "" + } return sp.schema.GetValue().GetReference() } @@ -332,6 +348,12 @@ func (sp *SchemaProxy) GetReferenceNode() *yaml.Node { if sp.refStr != "" { return utils.CreateRefNode(sp.refStr) } + if sp.isParsedRefWithSiblings() { + return sp.schema.Value.TransformedRef + } + if sp.schema == nil || sp.schema.Value == nil { + return nil + } return sp.schema.GetValue().GetReferenceNode() } @@ -397,6 +419,69 @@ func (sp *SchemaProxy) isRefWithSiblings() bool { return sp.refStr != "" && sp.rendered != nil && sp.schema == nil } +// IsTransformedRefWithSiblings reports whether this high-level proxy represents +// an authored OpenAPI 3.1 $ref with sibling schema keywords. +func (sp *SchemaProxy) IsTransformedRefWithSiblings() bool { + return sp != nil && + sp.schema != nil && + sp.schema.Value != nil && + sp.schema.Value.IsTransformedRefWithSiblings() && + sp.shouldCollapseTransformedRefWithSiblings() +} + +func (sp *SchemaProxy) isParsedRefWithSiblings() bool { + return sp.IsTransformedRefWithSiblings() +} + +func (sp *SchemaProxy) buildSiblingOnlySchemaView() *Schema { + if sp == nil || sp.schema == nil || sp.schema.Value == nil { + return nil + } + lowProxy := sp.schema.Value + siblingNode := lowProxy.GetTransformedRefSiblingSchema() + if siblingNode == nil { + return nil + } + + lowSchema := new(base.Schema) + if err := lowSchema.Build(lowProxy.GetContext(), siblingNode, lowProxy.GetIndex()); err != nil { + sp.buildError = err + return nil + } + lowSchema.ParentProxy = lowProxy + + schema := NewSchema(lowSchema) + schema.ParentProxy = sp + return schema +} + +// BuildTransformedRefSemanticSchema returns the internal semantic allOf view for +// an authored $ref-with-siblings proxy, using current high-level sibling values. +func (sp *SchemaProxy) BuildTransformedRefSemanticSchema(currentSibling *Schema) (*Schema, error) { + return sp.buildSemanticAllOfSchemaView(currentSibling) +} + +func (sp *SchemaProxy) buildSemanticAllOfSchemaView(currentSibling *Schema) (*Schema, error) { + if sp == nil || sp.schema == nil || sp.schema.Value == nil { + return nil, nil + } + lowSchema := sp.schema.Value.Schema() + if lowSchema == nil { + return nil, sp.schema.Value.GetBuildError() + } + schema := NewSchema(lowSchema) + schema.ParentProxy = nil + if currentSibling == nil { + currentSibling = sp.Schema() + } + if currentSibling != nil && len(schema.AllOf) == 2 { + siblingCopy := *currentSibling + siblingCopy.ParentProxy = nil + schema.AllOf[0] = CreateSchemaProxy(&siblingCopy) + } + return schema, nil +} + // renderRefWithSiblings builds a YAML mapping node containing $ref as the // first key followed by all rendered schema sibling properties. func (sp *SchemaProxy) renderRefWithSiblings() *yaml.Node { @@ -419,28 +504,37 @@ func (sp *SchemaProxy) renderTransformedRefWithSiblings(s *Schema) (*yaml.Node, if !sp.shouldCollapseTransformedRefWithSiblings() { return nil, false, nil } - if len(s.AllOf) != 2 || s.AllOf[0] == nil || s.AllOf[1] == nil || !s.AllOf[1].IsReference() { - return nil, false, nil - } - // Only collapse the synthetic allOf created by the sibling-ref transformer. - // If callers add fields to the outer schema or change its composition, keep - // the explicit allOf so no mutations are hidden. - outerNode := high.NewNodeBuilder(s, s.low).Render() - if len(outerNode.Content) != 2 || outerNode.Content[0].Value != "allOf" { - return nil, false, nil - } + var siblingNode *yaml.Node + ref := sp.schema.Value.GetTransformedRefReference() - siblingRender, err := s.AllOf[0].MarshalYAML() - if err != nil { - return nil, true, err - } - siblingNode, ok := yamlNodeFromRender(siblingRender) - if !ok || !utils.IsNodeMap(siblingNode) { - return nil, false, nil + if !sp.schemaIsTransformedSiblingView(s) { + if len(s.AllOf) != 2 || s.AllOf[0] == nil || s.AllOf[1] == nil || !s.AllOf[1].IsReference() { + return nil, false, nil + } + + // Only collapse the synthetic allOf created by the sibling-ref transformer. + // If callers add fields to the outer schema or change its composition, keep + // the explicit allOf so no mutations are hidden. + outerNode := high.NewNodeBuilder(s, s.low).Render() + if len(outerNode.Content) != 2 || outerNode.Content[0].Value != "allOf" { + return nil, false, nil + } + + siblingRender, err := s.AllOf[0].MarshalYAML() + if err != nil { + return nil, true, err + } + var ok bool + siblingNode, ok = yamlNodeFromRender(siblingRender) + if !ok || !utils.IsNodeMap(siblingNode) { + return nil, false, nil + } + ref = s.AllOf[1].GetReference() + } else { + siblingNode = high.NewNodeBuilder(s, s.low).Render() } - ref := s.AllOf[1].GetReference() original := sp.schema.Value.TransformedRef result := utils.CreateEmptyMapNode() consumed := make(map[string]struct{}, len(siblingNode.Content)/2) @@ -480,6 +574,13 @@ func (sp *SchemaProxy) renderTransformedRefWithSiblings(s *Schema) (*yaml.Node, return result, true, nil } +func (sp *SchemaProxy) schemaIsTransformedSiblingView(s *Schema) bool { + if sp == nil || sp.schema == nil || sp.schema.Value == nil || s == nil { + return false + } + return s.low != nil && s.low.RootNode == sp.schema.Value.GetTransformedRefSiblingSchema() +} + func (sp *SchemaProxy) shouldCollapseTransformedRefWithSiblings() bool { if sp == nil || sp.schema == nil || sp.schema.Value == nil { return false @@ -547,6 +648,9 @@ func (sp *SchemaProxy) Render() ([]byte, error) { // MarshalYAML will create a ready to render YAML representation of the SchemaProxy object. func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { + if sp.isParsedRefWithSiblings() { + return sp.referenceYAMLNodeForSchema(nil) + } if !sp.IsReference() { s, err := sp.BuildSchema() if err != nil { @@ -564,6 +668,29 @@ func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { return sp.GetReferenceNode(), nil } +func (sp *SchemaProxy) referenceYAMLNode() (*yaml.Node, error) { + return sp.referenceYAMLNodeForSchema(nil) +} + +func (sp *SchemaProxy) referenceYAMLNodeForSchema(s *Schema) (*yaml.Node, error) { + if sp.isRefWithSiblings() { + return sp.renderRefWithSiblings(), nil + } + if sp.isParsedRefWithSiblings() { + if s == nil { + var err error + s, err = sp.BuildSchema() + if err != nil { + return nil, err + } + } + if node, ok, renderErr := sp.renderTransformedRefWithSiblings(s); ok || renderErr != nil { + return node, renderErr + } + } + return sp.GetReferenceNode(), nil +} + // getInlineRenderKey generates a unique key for tracking this schema during inline rendering. // This prevents infinite recursion when schemas reference each other circularly. func (sp *SchemaProxy) getInlineRenderKey() string { @@ -575,6 +702,20 @@ func (sp *SchemaProxy) getInlineRenderKey() string { } return "" } + if sp.isParsedRefWithSiblings() && sp.schema.ValueNode != nil { + node := sp.schema.ValueNode + idx := sp.schema.Value.GetIndex() + if node.Line > 0 && node.Column > 0 { + if idx != nil { + return fmt.Sprintf("%s:%d:%d", idx.GetSpecAbsolutePath(), node.Line, node.Column) + } + return fmt.Sprintf("inline:%d:%d", node.Line, node.Column) + } + if idx != nil { + return fmt.Sprintf("%s:inline:%p", idx.GetSpecAbsolutePath(), node) + } + return fmt.Sprintf("inline:%p", node) + } // Use the reference string if available if sp.IsReference() { ref := sp.GetReference() @@ -632,13 +773,8 @@ func (sp *SchemaProxy) MarshalYAMLInline() (interface{}, error) { } func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (interface{}, error) { - // refNode returns the correct reference YAML node — with sibling - // properties when this proxy carries both a $ref and schema data. - refNode := func() *yaml.Node { - if sp.isRefWithSiblings() { - return sp.renderRefWithSiblings() - } - return sp.GetReferenceNode() + refNode := func() (*yaml.Node, error) { + return sp.referenceYAMLNode() } // check if this reference should be preserved (set via context by discriminator handling). @@ -647,7 +783,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte if sp.IsReference() { ref := sp.GetReference() if ref != "" && ctx.ShouldPreserveRef(ref) { - return refNode(), nil + return refNode() } } @@ -668,7 +804,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte rootIdx := rolodex.GetRootIndex() // If the schema is in the root index, preserve the ref if rootIdx != nil && schemaIdx == rootIdx { - return refNode(), nil + return refNode() } } } @@ -684,8 +820,9 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte if ctx.StartRendering(renderKey) { // We're already rendering this schema in THIS call chain - return ref to break the cycle if sp.IsReference() { - return refNode(), - fmt.Errorf("schema render failure, circular reference: `%s`", sp.GetReference()) + node, refErr := refNode() + return node, errors.Join(refErr, + fmt.Errorf("schema render failure, circular reference: `%s`", sp.GetReference())) } // For inline schemas, return an empty map to avoid infinite recursion return &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, @@ -716,7 +853,8 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte for _, c := range circ { if sp.IsReference() { if sp.GetReference() == c.LoopPoint.Definition { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } basePath := idx.GetSpecAbsolutePath() @@ -725,7 +863,8 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte } if basePath == c.LoopPoint.FullDefinition { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } a := utils.ReplaceWindowsDriveWithLinuxPath(strings.Replace(c.LoopPoint.FullDefinition, basePath, "", 1)) b := sp.GetReference() @@ -747,14 +886,16 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte bBase, bFragment := index.SplitRefFragment(b) if aFragment != "" && bFragment != "" && aFragment == bFragment { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } if aFragment == "" && bFragment == "" { aNorm := strings.TrimPrefix(strings.TrimPrefix(aBase, "./"), "/") bNorm := strings.TrimPrefix(strings.TrimPrefix(bBase, "./"), "/") if aNorm != "" && bNorm != "" && aNorm == bNorm { - return refNode(), cirError(c.LoopPoint.Definition) + node, refErr := refNode() + return node, errors.Join(refErr, cirError(c.LoopPoint.Definition)) } } } @@ -765,6 +906,9 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte return nil, err } if s != nil { + if sp.isParsedRefWithSiblings() { + return sp.marshalParsedRefWithSiblingsInline(ctx, s) + } // For programmatic ref+siblings proxies, render directly to avoid nil-deref // in Schema.MarshalYAMLInlineWithContext which assumes s.GoLow() is non-nil. if sp.isRefWithSiblings() { @@ -778,3 +922,14 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte } return nil, errors.New("unable to render schema") } + +func (sp *SchemaProxy) marshalParsedRefWithSiblingsInline(ctx *InlineRenderContext, currentSibling *Schema) (interface{}, error) { + s, err := sp.buildSemanticAllOfSchemaView(currentSibling) + if err != nil { + return nil, err + } + if s == nil { + return nil, errors.New("unable to render transformed schema reference") + } + return s.MarshalYAMLInlineWithContext(ctx) +} diff --git a/datamodel/high/base/schema_proxy_test.go b/datamodel/high/base/schema_proxy_test.go index 79e80b497..9581484fe 100644 --- a/datamodel/high/base/schema_proxy_test.go +++ b/datamodel/high/base/schema_proxy_test.go @@ -1783,6 +1783,152 @@ func TestSchemaProxy_MarshalYAML_TransformedRefWithSiblings(t *testing.T) { assert.Equal(t, "updated description", node.Content[3].Value) } +func TestSchemaProxy_ParsedTransformedRefWithSiblingsRestoresReferenceContract(t *testing.T) { + sp := buildParsedSiblingRefProxy(t, 3.1) + + assert.True(t, sp.isParsedRefWithSiblings()) + assert.True(t, sp.IsReference()) + assert.Equal(t, "#/components/schemas/Thing", sp.GetReference()) + require.NotNil(t, sp.GetReferenceNode()) + assert.Equal(t, "$ref", sp.GetReferenceNode().Content[0].Value) + + schema := sp.Schema() + require.NotNil(t, schema) + assert.Equal(t, "original description", schema.Description) + assert.Equal(t, "Original title", schema.Title) + assert.Empty(t, schema.AllOf) + assert.Equal(t, sp, schema.ParentProxy) + + schema.Description = "updated description" + rendered, err := sp.MarshalYAML() + require.NoError(t, err) + node, ok := rendered.(*yaml.Node) + require.True(t, ok) + require.Len(t, node.Content, 6) + assert.Equal(t, "$ref", node.Content[0].Value) + assert.Equal(t, "#/components/schemas/Thing", node.Content[1].Value) + assert.Equal(t, "description", node.Content[2].Value) + assert.Equal(t, "updated description", node.Content[3].Value) + assert.Equal(t, "title", node.Content[4].Value) + assert.Equal(t, "Original title", node.Content[5].Value) + + sp.schema.ValueNode = &yaml.Node{Kind: yaml.MappingNode} + assert.Contains(t, sp.getInlineRenderKey(), ":inline:") +} + +func TestSchemaProxy_ParsedTransformedRefWithSiblingsInlinePreservesSemantics(t *testing.T) { + sp := buildParsedSiblingRefProxy(t, 3.1) + + siblingSchema := sp.Schema() + require.NotNil(t, siblingSchema) + siblingSchema.Description = "mutated description" + + semanticSchema, err := sp.BuildTransformedRefSemanticSchema(siblingSchema) + require.NoError(t, err) + require.NotNil(t, semanticSchema) + require.Len(t, semanticSchema.AllOf, 2) + assert.Equal(t, "mutated description", semanticSchema.AllOf[0].Schema().Description) + assert.Equal(t, "string", semanticSchema.AllOf[1].Schema().Type[0]) + + semanticSchema, err = sp.BuildTransformedRefSemanticSchema(nil) + require.NoError(t, err) + require.NotNil(t, semanticSchema) + require.Len(t, semanticSchema.AllOf, 2) + assert.Equal(t, "mutated description", semanticSchema.AllOf[0].Schema().Description) + + rendered, err := sp.MarshalYAMLInline() + require.NoError(t, err) + node, ok := rendered.(*yaml.Node) + require.True(t, ok) + + out, err := yaml.Marshal(node) + require.NoError(t, err) + assert.Contains(t, string(out), "allOf:") + assert.Contains(t, string(out), "description: mutated description") + assert.NotContains(t, string(out), "description: original description") + assert.Contains(t, string(out), "type: string") + + inlineFromSchema, err := siblingSchema.MarshalYAMLInline() + require.NoError(t, err) + inlineNode, ok := inlineFromSchema.(*yaml.Node) + require.True(t, ok) + inlineOut, err := yaml.Marshal(inlineNode) + require.NoError(t, err) + assert.Contains(t, string(inlineOut), "allOf:") + assert.Contains(t, string(inlineOut), "description: mutated description") + assert.Contains(t, string(inlineOut), "type: string") +} + +func TestSchemaProxy_ParsedTransformedRefWithSiblingsJSONPreservesReference(t *testing.T) { + sp := buildParsedSiblingRefProxy(t, 3.1) + schema := sp.Schema() + require.NotNil(t, schema) + schema.Description = "json description" + + jsonBytes, err := schema.MarshalJSON() + require.NoError(t, err) + assert.JSONEq(t, `{ + "$ref": "#/components/schemas/Thing", + "description": "json description", + "title": "Original title" + }`, string(jsonBytes)) + + inlineBytes, err := schema.MarshalJSONInline() + require.NoError(t, err) + assert.Contains(t, string(inlineBytes), `"allOf"`) + assert.Contains(t, string(inlineBytes), `"description":"json description"`) + assert.Contains(t, string(inlineBytes), `"type":"string"`) +} + +func TestSchemaProxy_ParsedTransformedRefWithSiblingsOpenAPI30KeepsAllOfContract(t *testing.T) { + sp := buildParsedSiblingRefProxy(t, 3.0) + + assert.False(t, sp.isParsedRefWithSiblings()) + assert.False(t, sp.IsReference()) + assert.Empty(t, sp.GetReference()) + + schema := sp.Schema() + require.NotNil(t, schema) + require.Len(t, schema.AllOf, 2) + assert.Equal(t, "original description", schema.AllOf[0].Schema().Description) + assert.True(t, schema.AllOf[1].IsReference()) + assert.Equal(t, "#/components/schemas/Thing", schema.AllOf[1].GetReference()) +} + +func TestSchemaProxy_ParsedTransformedRefWithSiblingsFallbacks(t *testing.T) { + assert.False(t, (*SchemaProxy)(nil).isParsedRefWithSiblings()) + assert.Nil(t, (*SchemaProxy)(nil).buildSiblingOnlySchemaView()) + assert.False(t, (*SchemaProxy)(nil).schemaIsTransformedSiblingView(&Schema{})) + synthetic, err := (*SchemaProxy)(nil).buildSemanticAllOfSchemaView(nil) + require.NoError(t, err) + assert.Nil(t, synthetic) + + empty := &SchemaProxy{} + assert.False(t, empty.isParsedRefWithSiblings()) + assert.Nil(t, empty.GetReferenceNode()) + assert.Empty(t, empty.GetReference()) + _, err = empty.marshalParsedRefWithSiblingsInline(NewInlineRenderContext(), nil) + require.Error(t, err) + + lowProxyWithoutSiblingNode := &lowbase.SchemaProxy{TransformedRef: utils.CreateRefNode("#/components/schemas/Thing")} + withoutSiblingNode := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{Value: lowProxyWithoutSiblingNode}) + assert.Nil(t, withoutSiblingNode.buildSiblingOnlySchemaView()) + + emptyLowProxy := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{Value: &lowbase.SchemaProxy{}}) + synthetic, err = emptyLowProxy.buildSemanticAllOfSchemaView(nil) + require.Error(t, err) + assert.Nil(t, synthetic) + _, err = emptyLowProxy.marshalParsedRefWithSiblingsInline(NewInlineRenderContext(), nil) + require.Error(t, err) + + invalid := buildInvalidParsedSiblingRefProxy(t) + assert.Nil(t, invalid.Schema()) + require.Error(t, invalid.GetBuildError()) + node, err := invalid.referenceYAMLNode() + require.Error(t, err) + assert.Nil(t, node) +} + func TestSchemaProxy_RenderTransformedRefWithSiblingsFallbacks(t *testing.T) { deprecated := true lowProxy := &lowbase.SchemaProxy{ @@ -1844,6 +1990,101 @@ func TestSchemaProxy_RenderTransformedRefWithSiblingsFallbacks(t *testing.T) { assert.Nil(t, node) } +func buildParsedSiblingRefProxy(t *testing.T, version float32) *SchemaProxy { + t.Helper() + + spec := `openapi: 3.1.0 +paths: {} +components: + schemas: + Thing: + type: string + Holder: + type: object + properties: + thing: + $ref: '#/components/schemas/Thing' + description: original description + title: Original title +` + + var root yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(spec), &root)) + rootNode := root.Content[0] + + cfg := index.CreateOpenAPIIndexConfig() + cfg.SpecInfo = &datamodel.SpecInfo{VersionNumeric: version} + cfg.TransformSiblingRefs = true + idx := index.NewSpecIndexWithConfig(rootNode, cfg) + + refNode := findHighSchemaTestNode(t, rootNode, "components", "schemas", "Holder", "properties", "thing") + lowProxy := new(lowbase.SchemaProxy) + require.NoError(t, lowProxy.Build(context.Background(), nil, refNode, idx)) + require.NotNil(t, lowProxy.TransformedRef) + + return NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: lowProxy, + ValueNode: refNode, + }) +} + +func buildInvalidParsedSiblingRefProxy(t *testing.T) *SchemaProxy { + t.Helper() + + spec := `openapi: 3.1.0 +paths: {} +components: + schemas: + Thing: + type: string + Holder: + type: object + properties: + thing: + $ref: '#/components/schemas/Thing' + dependentRequired: + bad: nope +` + + var root yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(spec), &root)) + rootNode := root.Content[0] + + cfg := index.CreateOpenAPIIndexConfig() + cfg.SpecInfo = &datamodel.SpecInfo{VersionNumeric: 3.1} + cfg.TransformSiblingRefs = true + idx := index.NewSpecIndexWithConfig(rootNode, cfg) + + refNode := findHighSchemaTestNode(t, rootNode, "components", "schemas", "Holder", "properties", "thing") + lowProxy := new(lowbase.SchemaProxy) + require.NoError(t, lowProxy.Build(context.Background(), nil, refNode, idx)) + + return NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: lowProxy, + ValueNode: refNode, + }) +} + +func findHighSchemaTestNode(t *testing.T, node *yaml.Node, path ...string) *yaml.Node { + t.Helper() + + current := node + for _, key := range path { + require.NotNil(t, current) + require.Equal(t, yaml.MappingNode, current.Kind) + var next *yaml.Node + for i := 0; i+1 < len(current.Content); i += 2 { + if current.Content[i].Value == key { + next = current.Content[i+1] + break + } + } + require.NotNilf(t, next, "missing path key %q", key) + current = next + } + return current +} + func TestSchemaProxy_RenderTransformedRefWithSiblings_OpenAPI30Fallback(t *testing.T) { var root yaml.Node require.NoError(t, yaml.Unmarshal([]byte("$ref: '#/components/schemas/Thing'\nminLength: 2"), &root)) diff --git a/datamodel/high/base/schema_test.go b/datamodel/high/base/schema_test.go index e76577b69..f770c08e8 100644 --- a/datamodel/high/base/schema_test.go +++ b/datamodel/high/base/schema_test.go @@ -925,6 +925,20 @@ description: something object assert.Equal(t, `{"description":"something object","type":"object"}`, string(schemaBytes)) } +func TestMarshalYAMLRenderJSONErrors(t *testing.T) { + _, err := marshalYAMLRenderJSON("not a YAML node") + require.ErrorContains(t, err, "YAML render was not a node") + + _, err = marshalYAMLNodeJSON(&yaml.Node{ + Kind: yaml.SequenceNode, + Tag: "!!seq", + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Tag: "!!str", Value: "not a map"}, + }, + }) + require.Error(t, err) +} + func TestNewSchemaProxy_RenderSchemaWithMultipleObjectTypes(t *testing.T) { testSpec := `type: object description: something object diff --git a/datamodel/low/base/schema_build_coverage_test.go b/datamodel/low/base/schema_build_coverage_test.go index 81a4ecd56..43eaf8109 100644 --- a/datamodel/low/base/schema_build_coverage_test.go +++ b/datamodel/low/base/schema_build_coverage_test.go @@ -86,8 +86,9 @@ func TestTransformSiblingRefNode(t *testing.T) { var siblingRef yaml.Node require.NoError(t, yaml.Unmarshal([]byte("$ref: '#/components/schemas/Name'\ndeprecated: true"), &siblingRef)) - transformed, ok := transformSiblingRefNode(siblingRef.Content[0], nil) + transformed, metadata, ok := transformSiblingRefNode(siblingRef.Content[0], nil) require.False(t, ok) + assert.Nil(t, metadata) assert.Equal(t, siblingRef.Content[0], transformed) cfg := index.CreateOpenAPIIndexConfig() @@ -96,15 +97,18 @@ func TestTransformSiblingRefNode(t *testing.T) { var refOnly yaml.Node require.NoError(t, yaml.Unmarshal([]byte("$ref: '#/components/schemas/Name'"), &refOnly)) - transformed, ok = transformSiblingRefNode(refOnly.Content[0], idx) + transformed, metadata, ok = transformSiblingRefNode(refOnly.Content[0], idx) require.False(t, ok) + assert.Nil(t, metadata) assert.Equal(t, refOnly.Content[0], transformed) - transformed, ok = transformSiblingRefNode(siblingRef.Content[0], idx) + transformed, metadata, ok = transformSiblingRefNode(siblingRef.Content[0], idx) require.True(t, ok) require.NotNil(t, transformed) + require.NotNil(t, metadata) require.Len(t, transformed.Content, 2) assert.Equal(t, "allOf", transformed.Content[0].Value) + assert.Equal(t, "#/components/schemas/Name", metadata.reference) } func TestResolveSchemaBuildInput_TransformsSiblingRefBeforeResolution(t *testing.T) { @@ -138,12 +142,19 @@ components: assert.Equal(t, "allOf", resolved.valueNode.Content[0].Value) assert.Equal(t, fooNode, resolved.scopeNode) assert.Nil(t, resolved.refNode) - assert.Equal(t, fooNode, resolved.transformed) + require.NotNil(t, resolved.transformed) + assert.Equal(t, fooNode, resolved.transformed.referenceNode) assert.Empty(t, resolved.refLocation) assert.Equal(t, idx, resolved.idx) built := buildSchemaProxy(resolved.ctx, resolved.idx, fooNode, resolved.valueNode, resolved.scopeNode, resolved.refNode, resolved.transformed, resolved.refLocation) assert.Equal(t, fooNode, built.Value.TransformedRef) + assert.True(t, built.Value.IsTransformedRefWithSiblings()) + assert.Equal(t, "#/components/schemas/Name", built.Value.GetTransformedRefReference()) + assert.Equal(t, "allOf", built.Value.GetTransformedRefAllOfSchema().Content[0].Value) + require.NotNil(t, built.Value.GetTransformedRefSiblingSchema()) + require.Len(t, built.Value.GetTransformedRefSiblingSchema().Content, 2) + assert.Equal(t, "deprecated", built.Value.GetTransformedRefSiblingSchema().Content[0].Value) } func findNestedSchemaTestNode(t *testing.T, node *yaml.Node, path ...string) *yaml.Node { diff --git a/datamodel/low/base/schema_build_helpers.go b/datamodel/low/base/schema_build_helpers.go index 9f7f21fb7..03328453f 100644 --- a/datamodel/low/base/schema_build_helpers.go +++ b/datamodel/low/base/schema_build_helpers.go @@ -21,7 +21,7 @@ type resolvedSchemaBuildInput struct { valueNode *yaml.Node scopeNode *yaml.Node refNode *yaml.Node - transformed *yaml.Node + transformed *transformedSiblingRef refLocation string } @@ -102,7 +102,7 @@ func (s *Schema) extractExtensions(root *yaml.Node) { } // buildSchemaProxy builds out a SchemaProxy for a single node. -func buildSchemaProxy(ctx context.Context, idx *index.SpecIndex, kn, vn, scopeNode, rf, transformed *yaml.Node, refLocation string) low.ValueReference[*SchemaProxy] { +func buildSchemaProxy(ctx context.Context, idx *index.SpecIndex, kn, vn, scopeNode, rf *yaml.Node, transformed *transformedSiblingRef, refLocation string) low.ValueReference[*SchemaProxy] { sp := new(SchemaProxy) sp.prepareForResolvedBuild(ctx, kn, vn, scopeNode, idx, refLocation, rf, transformed) return low.ValueReference[*SchemaProxy]{ @@ -195,9 +195,9 @@ func resolveSchemaBuildInput(ctx context.Context, valueNode *yaml.Node, idx *ind return resolved, nil } - if transformedValue, wasTransformed := transformSiblingRefNode(valueNode, idx); wasTransformed { + if transformedValue, transformedRef, wasTransformed := transformSiblingRefNode(valueNode, idx); wasTransformed { resolved.valueNode = transformedValue - resolved.transformed = valueNode + resolved.transformed = transformedRef return resolved, nil } diff --git a/datamodel/low/base/schema_proxy.go b/datamodel/low/base/schema_proxy.go index e6c21fe61..17f1fbeda 100644 --- a/datamodel/low/base/schema_proxy.go +++ b/datamodel/low/base/schema_proxy.go @@ -70,6 +70,7 @@ type SchemaProxy struct { nodeStore sync.Map nodeMap low.NodeMap TransformedRef *yaml.Node // Original node that contained the ref before transformation + transformedRef *transformedSiblingRef *low.NodeMap } @@ -85,9 +86,9 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in // transform sibling refs to allOf structure if enabled and applicable // this ensures sp.vn contains the pre-transformed YAML as the source of truth - transformedValue, wasTransformed := transformSiblingRefNode(value, idx) + transformedValue, transformedRef, wasTransformed := transformSiblingRefNode(value, idx) if wasTransformed { - sp.TransformedRef = value // store original node that had the ref + sp.setTransformedRef(transformedRef) } sp.vn = transformedValue @@ -108,27 +109,27 @@ func (sp *SchemaProxy) Build(ctx context.Context, key, value *yaml.Node, idx *in return nil } -func transformSiblingRefNode(value *yaml.Node, idx *index.SpecIndex) (*yaml.Node, bool) { +func transformSiblingRefNode(value *yaml.Node, idx *index.SpecIndex) (*yaml.Node, *transformedSiblingRef, bool) { if idx == nil || idx.GetConfig() == nil || !idx.GetConfig().TransformSiblingRefs { - return value, false + return value, nil, false } transformer := NewSiblingRefTransformer(idx) - if !transformer.ShouldTransform(value) { - return value, false + transformed := transformer.transformSiblingRefWithMetadata(value) + if transformed == nil { + return value, nil, false } - transformed, _ := transformer.TransformSiblingRef(value) - return transformed, true + return transformed.allOfNode, transformed, true } // prepareForResolvedBuild initializes proxy state when the caller has already resolved any reference metadata. // This avoids re-running the full Build ref-detection path for child-schema helpers that already did that work. -func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value, scopeNode *yaml.Node, idx *index.SpecIndex, refLocation string, refNode, transformed *yaml.Node) { +func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value, scopeNode *yaml.Node, idx *index.SpecIndex, refLocation string, refNode *yaml.Node, transformed *transformedSiblingRef) { sp.kn = key sp.idx = idx sp.vn = value sp.ctx = applySchemaIdScope(ctx, scopeNode, idx) sp.Reference = low.Reference{} - sp.TransformedRef = transformed + sp.setTransformedRef(transformed) if refLocation != "" { sp.SetReference(refLocation, refNode) } @@ -137,6 +138,47 @@ func (sp *SchemaProxy) prepareForResolvedBuild(ctx context.Context, key, value, sp.NodeMap = &sp.nodeMap } +func (sp *SchemaProxy) setTransformedRef(transformed *transformedSiblingRef) { + sp.transformedRef = transformed + sp.TransformedRef = nil + if transformed != nil { + sp.TransformedRef = transformed.referenceNode + } +} + +// IsTransformedRefWithSiblings reports whether this proxy was authored as a +// schema-level $ref with sibling keywords and internally normalized to allOf. +func (sp *SchemaProxy) IsTransformedRefWithSiblings() bool { + return sp != nil && sp.transformedRef != nil && sp.transformedRef.reference != "" +} + +// GetTransformedRefSiblingSchema returns the sibling-only schema for an +// internally transformed $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefSiblingSchema() *yaml.Node { + if !sp.IsTransformedRefWithSiblings() { + return nil + } + return sp.transformedRef.siblingNode +} + +// GetTransformedRefReference returns the original reference value for an +// internally transformed $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefReference() string { + if !sp.IsTransformedRefWithSiblings() { + return "" + } + return sp.transformedRef.reference +} + +// GetTransformedRefAllOfSchema returns the internal allOf schema for an +// authored $ref-with-siblings node. +func (sp *SchemaProxy) GetTransformedRefAllOfSchema() *yaml.Node { + if !sp.IsTransformedRefWithSiblings() { + return nil + } + return sp.transformedRef.allOfNode +} + func applySchemaIdScope(ctx context.Context, node *yaml.Node, idx *index.SpecIndex) context.Context { if node == nil { return ctx diff --git a/datamodel/low/base/schema_proxy_test.go b/datamodel/low/base/schema_proxy_test.go index ac98e0c19..a0988064b 100644 --- a/datamodel/low/base/schema_proxy_test.go +++ b/datamodel/low/base/schema_proxy_test.go @@ -16,6 +16,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" ) @@ -525,6 +526,18 @@ $ref: "#/components/schemas/Base"`), &node) // verify TransformedRef was set (lines 87 in the if transformed != nil block) assert.NotNil(t, sp.TransformedRef, "TransformedRef should be set when transformation occurs") assert.Equal(t, node.Content[0], sp.TransformedRef, "TransformedRef should point to original node") + assert.True(t, sp.IsTransformedRefWithSiblings()) + assert.Equal(t, "#/components/schemas/Base", sp.GetTransformedRefReference()) + require.NotNil(t, sp.GetTransformedRefAllOfSchema()) + assert.Equal(t, "allOf", sp.GetTransformedRefAllOfSchema().Content[0].Value) + require.NotNil(t, sp.GetTransformedRefSiblingSchema()) + require.Len(t, sp.GetTransformedRefSiblingSchema().Content, 2) + assert.Equal(t, "title", sp.GetTransformedRefSiblingSchema().Content[0].Value) + assert.Equal(t, "Test", sp.GetTransformedRefSiblingSchema().Content[1].Value) + + assert.Nil(t, (*SchemaProxy)(nil).GetTransformedRefSiblingSchema()) + assert.Empty(t, (*SchemaProxy)(nil).GetTransformedRefReference()) + assert.Nil(t, (*SchemaProxy)(nil).GetTransformedRefAllOfSchema()) } func TestSchemaProxy_attemptPropertyMerging_SuccessfulMerge(t *testing.T) { diff --git a/datamodel/low/base/sibling_ref_transformer.go b/datamodel/low/base/sibling_ref_transformer.go index 8343a1373..69c9cd48e 100644 --- a/datamodel/low/base/sibling_ref_transformer.go +++ b/datamodel/low/base/sibling_ref_transformer.go @@ -17,6 +17,13 @@ type SiblingRefTransformer struct { index *index.SpecIndex } +type transformedSiblingRef struct { + allOfNode *yaml.Node + siblingNode *yaml.Node + referenceNode *yaml.Node + reference string +} + // NewSiblingRefTransformer creates a new transformer instance func NewSiblingRefTransformer(idx *index.SpecIndex) *SiblingRefTransformer { return &SiblingRefTransformer{ @@ -30,31 +37,35 @@ func NewSiblingRefTransformer(idx *index.SpecIndex) *SiblingRefTransformer { // Input: {title: "MySchema", $ref: "#/components/schemas/Base"} // Output: {allOf: [{title: "MySchema"}, {$ref: "#/components/schemas/Base"}]} func (srt *SiblingRefTransformer) TransformSiblingRef(node *yaml.Node) (*yaml.Node, error) { - if !srt.ShouldTransform(node) { + transformed := srt.transformSiblingRefWithMetadata(node) + if transformed == nil { return node, nil // no transformation needed } + return transformed.allOfNode, nil +} +func (srt *SiblingRefTransformer) transformSiblingRefWithMetadata(node *yaml.Node) *transformedSiblingRef { + if srt.index == nil || srt.index.GetConfig() == nil || !srt.index.GetConfig().TransformSiblingRefs { + return nil + } siblings, refValue := srt.ExtractSiblingProperties(node) - return srt.CreateAllOfStructure(refValue, siblings), nil + if len(siblings) == 0 || refValue == "" { + return nil + } + siblingNode := srt.createSiblingSchemaNode(node) + return &transformedSiblingRef{ + allOfNode: srt.createAllOfStructureWithSiblingNode(refValue, siblingNode), + siblingNode: siblingNode, + referenceNode: node, + reference: refValue, + } } // CreateAllOfStructure creates an allOf node structure from ref value and sibling properties func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings map[string]*yaml.Node) *yaml.Node { - - allOfNode := &yaml.Node{ - Kind: yaml.MappingNode, - Tag: "!!map", - Content: []*yaml.Node{ - {Kind: yaml.ScalarNode, Tag: "!!str", Value: "allOf"}, - {Kind: yaml.SequenceNode, Tag: "!!seq", Content: []*yaml.Node{}}, - }, - } - - allOfArrayNode := allOfNode.Content[1] - - // first element: schema with sibling properties (excluding $ref) + var siblingSchemaNode *yaml.Node if len(siblings) > 0 { - siblingSchemaNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + siblingSchemaNode = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} keys := make([]string, 0, len(siblings)) for key := range siblings { keys = append(keys, key) @@ -63,14 +74,29 @@ func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings for _, key := range keys { valueNode := siblings[key] keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key} - // create a copy of the value node to avoid modifying original copiedValueNode := srt.copyNode(valueNode) siblingSchemaNode.Content = append(siblingSchemaNode.Content, keyNode, copiedValueNode) } + } + return srt.createAllOfStructureWithSiblingNode(refValue, siblingSchemaNode) +} + +func (srt *SiblingRefTransformer) createAllOfStructureWithSiblingNode(refValue string, siblingSchemaNode *yaml.Node) *yaml.Node { + allOfNode := &yaml.Node{ + Kind: yaml.MappingNode, + Tag: "!!map", + Content: []*yaml.Node{ + {Kind: yaml.ScalarNode, Tag: "!!str", Value: "allOf"}, + {Kind: yaml.SequenceNode, Tag: "!!seq", Content: []*yaml.Node{}}, + }, + } + + allOfArrayNode := allOfNode.Content[1] + + if siblingSchemaNode != nil && len(siblingSchemaNode.Content) > 0 { allOfArrayNode.Content = append(allOfArrayNode.Content, siblingSchemaNode) } - // second element: the reference schema refSchemaNode := &yaml.Node{ Kind: yaml.MappingNode, Tag: "!!map", @@ -84,6 +110,22 @@ func (srt *SiblingRefTransformer) CreateAllOfStructure(refValue string, siblings return allOfNode } +func (srt *SiblingRefTransformer) createSiblingSchemaNode(node *yaml.Node) *yaml.Node { + if !utils.IsNodeMap(node) { + return nil + } + siblingNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + for i := 0; i+1 < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + if keyNode == nil || keyNode.Value == "$ref" { + continue + } + siblingNode.Content = append(siblingNode.Content, srt.copyNode(keyNode), srt.copyNode(valueNode)) + } + return siblingNode +} + // ExtractSiblingProperties extracts sibling properties from a node containing $ref // returns a map of sibling properties and the $ref value func (srt *SiblingRefTransformer) ExtractSiblingProperties(node *yaml.Node) (map[string]*yaml.Node, string) { diff --git a/datamodel/low/base/sibling_ref_transformer_test.go b/datamodel/low/base/sibling_ref_transformer_test.go index 538a9efe8..e4d2383fb 100644 --- a/datamodel/low/base/sibling_ref_transformer_test.go +++ b/datamodel/low/base/sibling_ref_transformer_test.go @@ -9,6 +9,7 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.yaml.in/yaml/v4" ) @@ -187,6 +188,30 @@ $ref: "#/components/schemas/Base"` assert.Equal(t, "allOf", result.Content[0].Value) allOfArray := result.Content[1] assert.Len(t, allOfArray.Content, 2) + + transformed := transformer.transformSiblingRefWithMetadata(actualNode) + require.NotNil(t, transformed) + assert.Equal(t, result.Content[0].Value, transformed.allOfNode.Content[0].Value) + assert.Equal(t, actualNode, transformed.referenceNode) + assert.Equal(t, "#/components/schemas/Base", transformed.reference) + require.NotNil(t, transformed.siblingNode) + require.Len(t, transformed.siblingNode.Content, 4) + assert.Equal(t, "title", transformed.siblingNode.Content[0].Value) + assert.Equal(t, "description", transformed.siblingNode.Content[2].Value) + + assert.Nil(t, transformer.createSiblingSchemaNode(&yaml.Node{Kind: yaml.ScalarNode, Value: "nope"})) + partial := transformer.createSiblingSchemaNode(&yaml.Node{ + Kind: yaml.MappingNode, + Content: []*yaml.Node{ + nil, + {Kind: yaml.ScalarNode, Value: "ignored"}, + {Kind: yaml.ScalarNode, Value: "title"}, + {Kind: yaml.ScalarNode, Value: "kept"}, + }, + }) + require.NotNil(t, partial) + require.Len(t, partial.Content, 2) + assert.Equal(t, "title", partial.Content[0].Value) }) t.Run("no transformation for ref only", func(t *testing.T) { diff --git a/document_test.go b/document_test.go index d68152555..1c2bb9f70 100644 --- a/document_test.go +++ b/document_test.go @@ -1927,8 +1927,7 @@ components: renderedStr := string(renderedBytes) - // Default rendering preserves the authored $ref+sibling syntax while the - // model still carries the transformed allOf semantics. + // Default rendering preserves the authored $ref+sibling syntax. assert.Equal(t, spec+"\n", renderedStr) assert.NotContains(t, renderedStr, "allOf:") assert.Contains(t, renderedStr, "title: destination-amazon-sqs") @@ -1937,19 +1936,13 @@ components: // verify the reloaded model has the correct structure if reloadedV3Doc.Model.Components != nil && reloadedV3Doc.Model.Components.Schemas != nil { if destinationSchema, found := reloadedV3Doc.Model.Components.Schemas.Get("destination-amazon-sqs"); found { + assert.True(t, destinationSchema.IsReference()) + assert.Equal(t, "#/components/schemas/destination-base", destinationSchema.GetReference()) + schema := destinationSchema.Schema() assert.NotNil(t, schema, "destination-amazon-sqs schema should exist") - assert.Len(t, schema.AllOf, 2, "should have 2 allOf items") - - // verify first allOf item has title - firstItem := schema.AllOf[0].Schema() - assert.NotNil(t, firstItem) - assert.Equal(t, "destination-amazon-sqs", firstItem.Title) - - // verify second allOf item is reference - secondItem := schema.AllOf[1] - assert.True(t, secondItem.IsReference()) - assert.Equal(t, "#/components/schemas/destination-base", secondItem.GetReference()) + assert.Empty(t, schema.AllOf) + assert.Equal(t, "destination-amazon-sqs", schema.Title) } else { t.Fatal("destination-amazon-sqs schema not found in reloaded model") } @@ -1992,8 +1985,7 @@ components: renderedStr := string(renderedBytes) - // Default rendering preserves the authored $ref+sibling syntax and order - // while the model still carries the transformed allOf semantics. + // Default rendering preserves the authored $ref+sibling syntax and order. assert.Equal(t, spec+"\n", renderedStr) assert.NotContains(t, renderedStr, "allOf:") assert.Contains(t, renderedStr, "$ref: '#/components/schemas/destination-base'") @@ -2002,19 +1994,13 @@ components: // verify the reloaded model has the correct structure if reloadedV3Doc.Model.Components != nil && reloadedV3Doc.Model.Components.Schemas != nil { if destinationSchema, found := reloadedV3Doc.Model.Components.Schemas.Get("destination-amazon-sqs"); found { + assert.True(t, destinationSchema.IsReference()) + assert.Equal(t, "#/components/schemas/destination-base", destinationSchema.GetReference()) + schema := destinationSchema.Schema() assert.NotNil(t, schema, "destination-amazon-sqs schema should exist") - assert.Len(t, schema.AllOf, 2, "should have 2 allOf items") - - // verify first allOf item has title - firstItem := schema.AllOf[0].Schema() - assert.NotNil(t, firstItem) - assert.Equal(t, "destination-amazon-sqs", firstItem.Description) - - // verify second allOf item is reference - secondItem := schema.AllOf[1] - assert.True(t, secondItem.IsReference()) - assert.Equal(t, "#/components/schemas/destination-base", secondItem.GetReference()) + assert.Empty(t, schema.AllOf) + assert.Equal(t, "destination-amazon-sqs", schema.Description) } else { t.Fatal("destination-amazon-sqs schema not found in reloaded model") } @@ -2166,24 +2152,18 @@ components: assert.True(t, config.TransformSiblingRefs, "TransformSiblingRefs should default to true even when using NewDocument()") - // Verify the sibling $ref was converted to allOf + // Verify the sibling $ref keeps the public ref contract while exposing + // sibling keywords directly on the local schema. withSiblings := model.Model.Components.Schemas.GetOrZero("WithSiblings") require.NotNil(t, withSiblings) + assert.True(t, withSiblings.IsReference()) + assert.Equal(t, "#/components/schemas/Base", withSiblings.GetReference()) schema := withSiblings.Schema() require.NotNil(t, schema) - require.NotNil(t, schema.AllOf, "sibling $ref should be transformed into allOf") - assert.Len(t, schema.AllOf, 2, "allOf should have 2 items: sibling props + $ref") - - // First allOf item should contain the sibling properties - siblingSchema := schema.AllOf[0].Schema() - require.NotNil(t, siblingSchema) - assert.Equal(t, "A constrained version of Base", siblingSchema.Description) - assert.Len(t, siblingSchema.Enum, 2) - - // Second allOf item should be the $ref - refItem := schema.AllOf[1] - assert.Equal(t, "#/components/schemas/Base", refItem.GetReference()) + assert.Empty(t, schema.AllOf) + assert.Equal(t, "A constrained version of Base", schema.Description) + assert.Len(t, schema.Enum, 2) } func TestNewDocument_TransformSiblingRefs_NestedSchemas(t *testing.T) { @@ -2240,33 +2220,33 @@ components: require.NotNil(t, model) topLevel := model.Model.Components.Schemas.GetOrZero("TopLevel") - assertSiblingRefAllOf(t, topLevel) + assertSiblingRefWithLocalKeyword(t, topLevel) container := model.Model.Components.Schemas.GetOrZero("Container").Schema() require.NotNil(t, container) - assertSiblingRefAllOf(t, container.Properties.GetOrZero("foo")) + assertSiblingRefWithLocalKeyword(t, container.Properties.GetOrZero("foo")) arrayContainer := model.Model.Components.Schemas.GetOrZero("ArrayContainer").Schema() require.NotNil(t, arrayContainer) require.NotNil(t, arrayContainer.Items) require.True(t, arrayContainer.Items.IsA()) - assertSiblingRefAllOf(t, arrayContainer.Items.A) + assertSiblingRefWithLocalKeyword(t, arrayContainer.Items.A) composed := model.Model.Components.Schemas.GetOrZero("Composed").Schema() require.NotNil(t, composed) require.Len(t, composed.AllOf, 1) - assertSiblingRefAllOf(t, composed.AllOf[0]) + assertSiblingRefWithLocalKeyword(t, composed.AllOf[0]) operation := model.Model.Paths.PathItems.GetOrZero("/things").Post require.NotNil(t, operation) require.Len(t, operation.Parameters, 1) - assertSiblingRefAllOf(t, operation.Parameters[0].Schema) + assertSiblingRefWithLocalKeyword(t, operation.Parameters[0].Schema) requestBody := operation.RequestBody require.NotNil(t, requestBody) mediaType := requestBody.Content.GetOrZero("application/json") require.NotNil(t, mediaType) - assertSiblingRefAllOf(t, mediaType.Schema) + assertSiblingRefWithLocalKeyword(t, mediaType.Schema) } func TestDocument_Render_Issue575_PreservesSiblingRefSyntax(t *testing.T) { @@ -2286,12 +2266,23 @@ func TestDocument_Render_Issue575_PreservesSiblingRefSyntax(t *testing.T) { require.NotNil(t, createTime) require.NotNil(t, createTime.GoLow()) assert.NotNil(t, createTime.GoLow().TransformedRef) + assert.True(t, createTime.IsReference()) + assert.Equal(t, "#/components/schemas/Timestamp", createTime.GetReference()) createTimeSchema := createTime.Schema() require.NotNil(t, createTimeSchema) - require.Len(t, createTimeSchema.AllOf, 2) - assert.Equal(t, "The creation timestamp of the shipper.", createTimeSchema.AllOf[0].Schema().Description) - assert.Equal(t, "#/components/schemas/Timestamp", createTimeSchema.AllOf[1].GetReference()) + assert.Empty(t, createTimeSchema.AllOf) + assert.Equal(t, "The creation timestamp of the shipper.", createTimeSchema.Description) + + updateTime := shipper.Properties.GetOrZero("updateTime") + require.NotNil(t, updateTime) + assert.True(t, updateTime.IsReference()) + assert.Equal(t, "#/components/schemas/Timestamp", updateTime.GetReference()) + updateTimeSchema := updateTime.Schema() + require.NotNil(t, updateTimeSchema) + assert.Equal(t, "The last update timestamp of the shipper.", updateTimeSchema.Title) + assert.Equal(t, "Updated when create/update/delete operation is performed.", updateTimeSchema.Description) + assert.Empty(t, updateTimeSchema.AllOf) schemaBytes, err := createTimeSchema.Render() require.NoError(t, err) @@ -2320,11 +2311,7 @@ func TestDocument_Render_Issue575_UsesMutatedSiblingValues(t *testing.T) { createTimeSchema := createTime.Schema() require.NotNil(t, createTimeSchema) - require.Len(t, createTimeSchema.AllOf, 2) - - siblingSchema := createTimeSchema.AllOf[0].Schema() - require.NotNil(t, siblingSchema) - siblingSchema.Description = "The created timestamp from libopenapi." + createTimeSchema.Description = "The created timestamp from libopenapi." modelBytes, err := model.Model.Render() require.NoError(t, err) @@ -2411,27 +2398,20 @@ components: assert.Contains(t, rendered, "- $ref: '#/components/schemas/Name'") } -func assertSiblingRefAllOf(t *testing.T, proxy *base.SchemaProxy) { +func assertSiblingRefWithLocalKeyword(t *testing.T, proxy *base.SchemaProxy) { t.Helper() require.NotNil(t, proxy) - assert.False(t, proxy.IsReference()) - assert.Empty(t, proxy.GetReference()) + assert.True(t, proxy.IsReference()) + assert.Equal(t, "#/components/schemas/Name", proxy.GetReference()) require.NotNil(t, proxy.GoLow()) assert.NotNil(t, proxy.GoLow().TransformedRef) schema := proxy.Schema() require.NotNil(t, schema) - require.Len(t, schema.AllOf, 2) - - siblingSchema := schema.AllOf[0].Schema() - require.NotNil(t, siblingSchema) - require.NotNil(t, siblingSchema.Deprecated) - assert.True(t, *siblingSchema.Deprecated) - - refItem := schema.AllOf[1] - assert.True(t, refItem.IsReference()) - assert.Equal(t, "#/components/schemas/Name", refItem.GetReference()) + assert.Empty(t, schema.AllOf) + require.NotNil(t, schema.Deprecated) + assert.True(t, *schema.Deprecated) } func TestDocument_Release(t *testing.T) { diff --git a/generator/golang/from_openapi.go b/generator/golang/from_openapi.go index b1d499a5e..f8e590d4d 100644 --- a/generator/golang/from_openapi.go +++ b/generator/golang/from_openapi.go @@ -21,6 +21,18 @@ func (g *Generator) irFromOpenAPIName(name string, nameResolved bool, proxy *hig if cached := g.openapiCache[proxy]; cached != nil { return cached, nil } + if proxy.IsTransformedRefWithSiblings() { + schema, err := proxy.BuildTransformedRefSemanticSchema(proxy.Schema()) + if err != nil { + return nil, wrapPath(err, path) + } + if schema == nil { + return nil, wrapPath(ErrNilSchema, path) + } + ir := g.irFromSchema(name, nameResolved, schema, path) + g.openapiCache[proxy] = ir + return ir, nil + } if proxy.IsReference() { ref := proxy.GetReference() typeName := g.refTypeName(ref) diff --git a/generator/golang/generator_test.go b/generator/golang/generator_test.go index 205587055..acb34f37b 100644 --- a/generator/golang/generator_test.go +++ b/generator/golang/generator_test.go @@ -500,7 +500,48 @@ func TestImplicitDiscriminatorJSON(t *testing.T) { t.Fatalf("unexpected pet value: %#v", pet.Value) } } + `) +} + +func TestRenderSchemasTransformedSiblingRefComponentIsNotPureReference(t *testing.T) { + spec := []byte(`openapi: 3.1.0 +info: + title: Test + version: 1.0.0 +paths: {} +components: + schemas: + Base: + type: string + WithSibling: + $ref: '#/components/schemas/Base' + description: constrained base value + enum: [fast, slow] `) + doc, err := libopenapi.NewDocument(spec) + if err != nil { + t.Fatal(err) + } + model, err := doc.BuildV3Model() + if err != nil { + t.Fatal(err) + } + withSibling := model.Model.Components.Schemas.GetOrZero("WithSibling") + if withSibling == nil || !withSibling.IsReference() || !withSibling.IsTransformedRefWithSiblings() { + t.Fatalf("expected transformed sibling ref component, got %#v", withSibling) + } + + file, err := NewGenerator(WithEnumConstants(true)).RenderSchemas(model.Model.Components.Schemas) + if err != nil { + t.Fatal(err) + } + src := string(file.Source) + assertContains(t, src, "type Base string") + assertContains(t, src, "type WithSibling ") + assertContains(t, src, "constrained base value") + assertContains(t, src, `"fast"`) + assertContains(t, src, `"slow"`) + assertParsesAndCompiles(t, file.Source) } func renderTrainTravel(t *testing.T, opts ...Option) *GeneratedFile { diff --git a/renderer/mock_render_context.go b/renderer/mock_render_context.go index 105cb1423..8add3637b 100644 --- a/renderer/mock_render_context.go +++ b/renderer/mock_render_context.go @@ -64,6 +64,12 @@ func (ctx *mockRenderContext) diveIntoSchema(schema *base.Schema, key string, st structure[key] = example return ctx.noteValue(example) } + if semantic, err := semanticSchemaForTransformedRef(schema); err != nil { + ctx.err = err + return false + } else if semantic != nil { + return ctx.diveIntoSchema(semantic, key, structure, depth) + } if !ctx.noteNode() { return false } @@ -114,6 +120,13 @@ func (ctx *mockRenderContext) diveIntoSchema(schema *base.Schema, key string, st return true } +func semanticSchemaForTransformedRef(schema *base.Schema) (*base.Schema, error) { + if schema == nil || schema.ParentProxy == nil || !schema.ParentProxy.IsTransformedRefWithSiblings() { + return nil, nil + } + return schema.ParentProxy.BuildTransformedRefSemanticSchema(schema) +} + func (ctx *mockRenderContext) renderString(schema *base.Schema, key string, structure map[string]any) bool { structure[key] = ctx.renderer.renderMockStringValue(schema, key, ctx.options.MaxGeneratedStringBytes) return ctx.noteValue(structure[key]) diff --git a/renderer/schema_renderer.go b/renderer/schema_renderer.go index 9e3b32384..9c3c8ba45 100644 --- a/renderer/schema_renderer.go +++ b/renderer/schema_renderer.go @@ -241,6 +241,11 @@ func (wr *SchemaRenderer) DiveIntoSchema(schema *base.Schema, key string, struct structure[key] = example return true } + if semantic, err := semanticSchemaForTransformedRef(schema); err != nil { + return false + } else if semantic != nil { + return wr.DiveIntoSchema(semantic, key, structure, visited, depth) + } // Prevent unbounded recursion on deeply nested schemas. if depth > 100 { diff --git a/renderer/schema_renderer_test.go b/renderer/schema_renderer_test.go index ef696a633..e20325f4c 100644 --- a/renderer/schema_renderer_test.go +++ b/renderer/schema_renderer_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + "github.com/pb33f/libopenapi" highbase "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/low" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" @@ -1400,6 +1401,36 @@ schemas: assert.Equal(t, `{"address":"Baker Street","owner":{"name":"John Doe"}}`, string(rendered)) } +func TestRenderSchema_RefWithSiblingsUsesReferencedShape(t *testing.T) { + spec := `openapi: 3.1.0 +info: + title: Ref Siblings + version: 1.0.0 +paths: {} +components: + schemas: + Base: + type: string + example: from-base + WithSibling: + $ref: '#/components/schemas/Base' + description: Local description +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + assert.NoError(t, err) + model, err := doc.BuildV3Model() + assert.NoError(t, err) + + proxy := model.Model.Components.Schemas.GetOrZero("WithSibling") + assert.True(t, proxy.IsReference()) + assert.Equal(t, "#/components/schemas/Base", proxy.GetReference()) + + wr := createSchemaRenderer() + rendered := wr.RenderSchema(proxy.Schema()) + assert.Equal(t, "from-base", rendered) +} + func TestRenderSchema_Ref_NoExample(t *testing.T) { yml := ` schemas: From 7e735f4d2e42d38ee2305c1f9b8c61f63783bc66 Mon Sep 17 00:00:00 2001 From: quobix Date: Wed, 27 May 2026 15:28:02 -0400 Subject: [PATCH 2/3] bump coverage --- datamodel/high/base/schema_proxy_test.go | 18 +++++++++ generator/golang/from_openapi.go | 3 -- generator/golang/generator_test.go | 47 ++++++++++++++++++++++++ renderer/schema_renderer_test.go | 43 ++++++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/datamodel/high/base/schema_proxy_test.go b/datamodel/high/base/schema_proxy_test.go index 9581484fe..5a0a06fc4 100644 --- a/datamodel/high/base/schema_proxy_test.go +++ b/datamodel/high/base/schema_proxy_test.go @@ -1880,6 +1880,24 @@ func TestSchemaProxy_ParsedTransformedRefWithSiblingsJSONPreservesReference(t *t assert.Contains(t, string(inlineBytes), `"type":"string"`) } +func TestSchemaProxy_ParsedTransformedRefWithSiblingsJSONReturnsErrors(t *testing.T) { + sp := buildParsedSiblingRefProxy(t, 3.1) + semanticSchema, err := sp.BuildTransformedRefSemanticSchema(sp.Schema()) + require.NoError(t, err) + require.NotNil(t, semanticSchema) + require.Len(t, semanticSchema.AllOf, 2) + semanticSchema.ParentProxy = sp + semanticSchema.AllOf[0] = &SchemaProxy{buildError: errors.New("boom")} + + jsonBytes, err := semanticSchema.MarshalJSON() + require.Error(t, err) + assert.Nil(t, jsonBytes) + + inlineBytes, err := semanticSchema.MarshalJSONInline() + require.Error(t, err) + assert.Nil(t, inlineBytes) +} + func TestSchemaProxy_ParsedTransformedRefWithSiblingsOpenAPI30KeepsAllOfContract(t *testing.T) { sp := buildParsedSiblingRefProxy(t, 3.0) diff --git a/generator/golang/from_openapi.go b/generator/golang/from_openapi.go index f8e590d4d..00b999ec5 100644 --- a/generator/golang/from_openapi.go +++ b/generator/golang/from_openapi.go @@ -26,9 +26,6 @@ func (g *Generator) irFromOpenAPIName(name string, nameResolved bool, proxy *hig if err != nil { return nil, wrapPath(err, path) } - if schema == nil { - return nil, wrapPath(ErrNilSchema, path) - } ir := g.irFromSchema(name, nameResolved, schema, path) g.openapiCache[proxy] = ir return ir, nil diff --git a/generator/golang/generator_test.go b/generator/golang/generator_test.go index acb34f37b..a758523ad 100644 --- a/generator/golang/generator_test.go +++ b/generator/golang/generator_test.go @@ -5,6 +5,7 @@ package golang import ( "bytes" + "context" "go/format" "go/parser" "go/token" @@ -17,7 +18,11 @@ import ( "time" "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel" highbase "github.com/pb33f/libopenapi/datamodel/high/base" + "github.com/pb33f/libopenapi/datamodel/low" + lowbase "github.com/pb33f/libopenapi/datamodel/low/base" + "github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/orderedmap" "go.yaml.in/yaml/v4" ) @@ -544,6 +549,48 @@ components: assertParsesAndCompiles(t, file.Source) } +func TestIRFromOpenAPITransformedSiblingRefBuildError(t *testing.T) { + proxy := malformedTransformedSiblingRefProxy(t) + if !proxy.IsTransformedRefWithSiblings() { + t.Fatal("expected transformed sibling ref") + } + + _, err := NewGenerator().run().irFromOpenAPI("WithSibling", proxy, "WithSibling") + if err == nil { + t.Fatal("expected transformed sibling ref build error") + } + if !strings.Contains(err.Error(), "WithSibling") { + t.Fatalf("expected error path to include component name, got %v", err) + } +} + +func malformedTransformedSiblingRefProxy(t *testing.T) *highbase.SchemaProxy { + t.Helper() + + var node yaml.Node + err := yaml.Unmarshal([]byte(`$ref: '#/components/schemas/Base' +dependentRequired: + bad: nope +`), &node) + if err != nil { + t.Fatal(err) + } + + cfg := index.CreateOpenAPIIndexConfig() + cfg.SpecInfo = &datamodel.SpecInfo{VersionNumeric: 3.1} + cfg.TransformSiblingRefs = true + idx := index.NewSpecIndexWithConfig(node.Content[0], cfg) + + lowProxy := new(lowbase.SchemaProxy) + if err := lowProxy.Build(context.Background(), nil, node.Content[0], idx); err != nil { + t.Fatal(err) + } + return highbase.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: lowProxy, + ValueNode: node.Content[0], + }) +} + func renderTrainTravel(t *testing.T, opts ...Option) *GeneratedFile { t.Helper() spec, err := os.ReadFile("testdata/train-travel.yaml") diff --git a/renderer/schema_renderer_test.go b/renderer/schema_renderer_test.go index e20325f4c..5691b922f 100644 --- a/renderer/schema_renderer_test.go +++ b/renderer/schema_renderer_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/pb33f/libopenapi" + "github.com/pb33f/libopenapi/datamodel" highbase "github.com/pb33f/libopenapi/datamodel/high/base" "github.com/pb33f/libopenapi/datamodel/low" lowbase "github.com/pb33f/libopenapi/datamodel/low/base" @@ -1429,6 +1430,48 @@ components: wr := createSchemaRenderer() rendered := wr.RenderSchema(proxy.Schema()) assert.Equal(t, "from-base", rendered) + + structure := make(map[string]any) + assert.True(t, wr.DiveIntoSchema(proxy.Schema(), "value", structure, createVisitedMap(), 0)) + assert.Equal(t, "from-base", structure["value"]) +} + +func TestRenderSchema_TransformedRefSemanticErrors(t *testing.T) { + schema := &highbase.Schema{ParentProxy: malformedRendererTransformedSiblingRefProxy(t)} + + ctx := newMockRenderContext(createSchemaRenderer()) + structure := make(map[string]any) + assert.False(t, ctx.diveIntoSchema(schema, rootType, structure, 0)) + assert.Error(t, ctx.err) + + assert.False(t, createSchemaRenderer().DiveIntoSchema(schema, rootType, structure, createVisitedMap(), 0)) +} + +func malformedRendererTransformedSiblingRefProxy(t *testing.T) *highbase.SchemaProxy { + t.Helper() + + var node yaml.Node + err := yaml.Unmarshal([]byte(`$ref: '#/components/schemas/Base' +dependentRequired: + bad: nope +`), &node) + if err != nil { + t.Fatal(err) + } + + cfg := index.CreateOpenAPIIndexConfig() + cfg.SpecInfo = &datamodel.SpecInfo{VersionNumeric: 3.1} + cfg.TransformSiblingRefs = true + idx := index.NewSpecIndexWithConfig(node.Content[0], cfg) + + lowProxy := new(lowbase.SchemaProxy) + if err := lowProxy.Build(context.Background(), nil, node.Content[0], idx); err != nil { + t.Fatal(err) + } + return highbase.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{ + Value: lowProxy, + ValueNode: node.Content[0], + }) } func TestRenderSchema_Ref_NoExample(t *testing.T) { From 911224d17f1913be8387959ab91cbd26f0258dd7 Mon Sep 17 00:00:00 2001 From: quobix Date: Wed, 27 May 2026 16:43:38 -0400 Subject: [PATCH 3/3] test: recover sibling ref patch coverage --- datamodel/high/base/schema_proxy.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index cee10e4e7..adc9afb5a 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -706,15 +706,17 @@ func (sp *SchemaProxy) getInlineRenderKey() string { node := sp.schema.ValueNode idx := sp.schema.Value.GetIndex() if node.Line > 0 && node.Column > 0 { + source := "inline" if idx != nil { - return fmt.Sprintf("%s:%d:%d", idx.GetSpecAbsolutePath(), node.Line, node.Column) + source = idx.GetSpecAbsolutePath() } - return fmt.Sprintf("inline:%d:%d", node.Line, node.Column) + return fmt.Sprintf("%s:%d:%d", source, node.Line, node.Column) } + source := "inline" if idx != nil { - return fmt.Sprintf("%s:inline:%p", idx.GetSpecAbsolutePath(), node) + source = fmt.Sprintf("%s:inline", idx.GetSpecAbsolutePath()) } - return fmt.Sprintf("inline:%p", node) + return fmt.Sprintf("%s:%p", source, node) } // Use the reference string if available if sp.IsReference() {