@@ -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.
206215func (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.
381411func (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.
386416func (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
469498func (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