Skip to content

Commit 6f86a18

Browse files
committed
Address issue #533
Adds a new `CreateSchemaProxyRefWithSchema` function.
1 parent 5bdaf86 commit 6f86a18

2 files changed

Lines changed: 298 additions & 21 deletions

File tree

datamodel/high/base/schema_proxy.go

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,15 @@ func CreateSchemaProxyRef(ref string) *SchemaProxy {
202202
return &SchemaProxy{refStr: ref, lock: &sync.Mutex{}}
203203
}
204204

205+
// CreateSchemaProxyRefWithSchema creates a SchemaProxy that carries both a $ref and sibling schema
206+
// properties. This supports JSON Schema 2020-12 section 7.7.1.1 where $ref can coexist with other
207+
// keywords. When rendered, $ref appears first followed by the schema's sibling properties.
208+
//
209+
// If schema is nil, the result behaves identically to CreateSchemaProxyRef.
210+
func CreateSchemaProxyRefWithSchema(ref string, schema *Schema) *SchemaProxy {
211+
return &SchemaProxy{refStr: ref, rendered: schema, lock: &sync.Mutex{}}
212+
}
213+
205214
// GetValueNode returns the value node of the SchemaProxy.
206215
func (sp *SchemaProxy) GetValueNode() *yaml.Node {
207216
if sp.schema != nil {
@@ -377,26 +386,46 @@ func (sp *SchemaProxy) GoLowUntyped() any {
377386
return sp.schema.Value
378387
}
379388

389+
// isRefWithSiblings returns true when this is a programmatically-created proxy
390+
// that carries both a $ref and sibling schema properties.
391+
func (sp *SchemaProxy) isRefWithSiblings() bool {
392+
return sp.refStr != "" && sp.rendered != nil && sp.schema == nil
393+
}
394+
395+
// renderRefWithSiblings builds a YAML mapping node containing $ref as the
396+
// first key followed by all rendered schema sibling properties.
397+
func (sp *SchemaProxy) renderRefWithSiblings() *yaml.Node {
398+
nb := high.NewNodeBuilder(sp.rendered, nil)
399+
node := nb.Render()
400+
refKey := utils.CreateStringNode("$ref")
401+
refVal := utils.CreateStringNode(sp.refStr)
402+
refVal.Style = yaml.SingleQuotedStyle
403+
content := make([]*yaml.Node, 0, len(node.Content)+2)
404+
content = append(content, refKey, refVal)
405+
content = append(content, node.Content...)
406+
node.Content = content
407+
return node
408+
}
409+
380410
// Render will return a YAML representation of the Schema object as a byte slice.
381411
func (sp *SchemaProxy) Render() ([]byte, error) {
382412
return yaml.Marshal(sp)
383413
}
384414

385415
// MarshalYAML will create a ready to render YAML representation of the SchemaProxy object.
386416
func (sp *SchemaProxy) MarshalYAML() (interface{}, error) {
387-
var s *Schema
388-
var err error
389-
// if this schema isn't a reference, then build it out.
390417
if !sp.IsReference() {
391-
s, err = sp.BuildSchema()
418+
s, err := sp.BuildSchema()
392419
if err != nil {
393420
return nil, err
394421
}
395422
nb := high.NewNodeBuilder(s, s.low)
396423
return nb.Render(), nil
397-
} else {
398-
return sp.GetReferenceNode(), nil
399424
}
425+
if sp.isRefWithSiblings() {
426+
return sp.renderRefWithSiblings(), nil
427+
}
428+
return sp.GetReferenceNode(), nil
400429
}
401430

402431
// getInlineRenderKey generates a unique key for tracking this schema during inline rendering.
@@ -467,13 +496,22 @@ func (sp *SchemaProxy) MarshalYAMLInline() (interface{}, error) {
467496
}
468497

469498
func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (interface{}, error) {
499+
// refNode returns the correct reference YAML node — with sibling
500+
// properties when this proxy carries both a $ref and schema data.
501+
refNode := func() *yaml.Node {
502+
if sp.isRefWithSiblings() {
503+
return sp.renderRefWithSiblings()
504+
}
505+
return sp.GetReferenceNode()
506+
}
507+
470508
// check if this reference should be preserved (set via context by discriminator handling).
471509
// this avoids mutating shared SchemaProxy state and prevents race conditions.
472510
// need to guard against nil schema.Value which can happen with bad/incomplete proxies.
473511
if sp.IsReference() {
474512
ref := sp.GetReference()
475513
if ref != "" && ctx.ShouldPreserveRef(ref) {
476-
return sp.GetReferenceNode(), nil
514+
return refNode(), nil
477515
}
478516
}
479517

@@ -494,7 +532,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte
494532
rootIdx := rolodex.GetRootIndex()
495533
// If the schema is in the root index, preserve the ref
496534
if rootIdx != nil && schemaIdx == rootIdx {
497-
return sp.GetReferenceNode(), nil
535+
return refNode(), nil
498536
}
499537
}
500538
}
@@ -510,7 +548,7 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte
510548
if ctx.StartRendering(renderKey) {
511549
// We're already rendering this schema in THIS call chain - return ref to break the cycle
512550
if sp.IsReference() {
513-
return sp.GetReferenceNode(),
551+
return refNode(),
514552
fmt.Errorf("schema render failure, circular reference: `%s`", sp.GetReference())
515553
}
516554
// For inline schemas, return an empty map to avoid infinite recursion
@@ -542,20 +580,16 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte
542580
for _, c := range circ {
543581
if sp.IsReference() {
544582
if sp.GetReference() == c.LoopPoint.Definition {
545-
// nope
546-
return sp.GetReferenceNode(),
547-
cirError((c.LoopPoint.Definition))
583+
return refNode(), cirError(c.LoopPoint.Definition)
548584
}
549-
basePath := sp.GoLow().GetIndex().GetSpecAbsolutePath()
585+
basePath := idx.GetSpecAbsolutePath()
550586

551587
if !filepath.IsAbs(basePath) && !strings.HasPrefix(basePath, "http") {
552588
basePath, _ = filepath.Abs(basePath)
553589
}
554590

555591
if basePath == c.LoopPoint.FullDefinition {
556-
// we loop on our-self
557-
return sp.GetReferenceNode(),
558-
cirError((c.LoopPoint.Definition))
592+
return refNode(), cirError(c.LoopPoint.Definition)
559593
}
560594
a := utils.ReplaceWindowsDriveWithLinuxPath(strings.Replace(c.LoopPoint.FullDefinition, basePath, "", 1))
561595
b := sp.GetReference()
@@ -577,17 +611,14 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte
577611
bBase, bFragment := index.SplitRefFragment(b)
578612

579613
if aFragment != "" && bFragment != "" && aFragment == bFragment {
580-
return sp.GetReferenceNode(),
581-
cirError((c.LoopPoint.Definition))
614+
return refNode(), cirError(c.LoopPoint.Definition)
582615
}
583616

584617
if aFragment == "" && bFragment == "" {
585618
aNorm := strings.TrimPrefix(strings.TrimPrefix(aBase, "./"), "/")
586619
bNorm := strings.TrimPrefix(strings.TrimPrefix(bBase, "./"), "/")
587620
if aNorm != "" && bNorm != "" && aNorm == bNorm {
588-
// nope
589-
return sp.GetReferenceNode(),
590-
cirError((c.LoopPoint.Definition))
621+
return refNode(), cirError(c.LoopPoint.Definition)
591622
}
592623
}
593624
}
@@ -598,6 +629,11 @@ func (sp *SchemaProxy) marshalYAMLInlineInternal(ctx *InlineRenderContext) (inte
598629
return nil, err
599630
}
600631
if s != nil {
632+
// For programmatic ref+siblings proxies, render directly to avoid nil-deref
633+
// in Schema.MarshalYAMLInlineWithContext which assumes s.GoLow() is non-nil.
634+
if sp.isRefWithSiblings() {
635+
return sp.renderRefWithSiblings(), nil
636+
}
601637
// Delegate to Schema.MarshalYAMLInlineWithContext to ensure discriminator handling is applied
602638
// and cycle detection context is propagated.
603639
// Schema.MarshalYAMLInlineWithContext sets preserveReference on OneOf/AnyOf items when

0 commit comments

Comments
 (0)