@@ -2,10 +2,12 @@ package oas3
22
33import (
44 "context"
5+ "strings"
56 "unsafe"
67
78 "github.com/speakeasy-api/openapi/extensions"
89 "github.com/speakeasy-api/openapi/jsonschema/oas3/core"
10+ "github.com/speakeasy-api/openapi/marshaller"
911 "github.com/speakeasy-api/openapi/pointer"
1012 "github.com/speakeasy-api/openapi/references"
1113 "github.com/speakeasy-api/openapi/validation"
@@ -38,6 +40,20 @@ type JSONSchema[T Referenceable | Concrete] struct {
3840 // parent = intermediate reference, topLevelParent = original reference
3941 parent * JSONSchema [Referenceable ] // Immediate parent reference in the chain
4042 topLevelParent * JSONSchema [Referenceable ] // Top-level parent (root of the reference chain)
43+
44+ // enclosingSchema is the Schema that contains this JSONSchema as a field.
45+ // This is used during population to determine the parent's effective base URI
46+ // for relative $id resolution. Set during PopulateWithContext.
47+ enclosingSchema * Schema
48+
49+ // schemaRegistry stores $id and $anchor mappings for this document.
50+ // Used for standalone JSON Schema documents that are not embedded in an OpenAPI document.
51+ // For embedded schemas, the owning OpenAPI document's registry is used instead.
52+ schemaRegistry SchemaRegistry
53+
54+ // documentBaseURI is the base URI for this standalone JSON Schema document.
55+ // This is typically derived from the $id keyword or empty if not specified.
56+ documentBaseURI string
4157}
4258
4359var _ references.Resolvable [JSONSchema [Concrete ]] = (* JSONSchema [Referenceable ])(nil )
@@ -281,6 +297,9 @@ func (j *JSONSchema[T]) ShallowCopy() *JSONSchema[T] {
281297 resolvedSchemaCache : j .resolvedSchemaCache ,
282298 parent : j .parent ,
283299 topLevelParent : j .topLevelParent ,
300+ enclosingSchema : j .enclosingSchema ,
301+ schemaRegistry : j .schemaRegistry ,
302+ documentBaseURI : j .documentBaseURI ,
284303 }
285304
286305 // Shallow copy the EitherValue contents
@@ -295,23 +314,137 @@ func (j *JSONSchema[T]) ShallowCopy() *JSONSchema[T] {
295314 return result
296315}
297316
298- // PopulateWithParent implements the ParentAwarePopulator interface to establish parent relationships during population
299- func (j * JSONSchema [T ]) PopulateWithParent (source any , parent any ) error {
300- // If we have a parent that is also a JSONSchema, establish the parent relationship
301- if parent != nil {
302- if parentSchema , ok := parent .(* Schema ); ok {
303- j .SetParent (parentSchema .GetParent ())
304- // If the parent has a top-level parent, inherit it; otherwise, the parent is the top-level
305- if parentSchema .GetParent ().GetTopLevelParent () != nil {
306- j .SetTopLevelParent (parentSchema .GetParent ().GetTopLevelParent ())
307- } else {
308- j .SetTopLevelParent (parentSchema .GetParent ())
309- }
317+ // GetSchemaRegistry returns the schema registry for this standalone JSON Schema document.
318+ // The registry stores $id and $anchor mappings for efficient schema resolution.
319+ // If the registry has not been initialized, it creates one with the document's base URI.
320+ // This implements the SchemaRegistryProvider interface.
321+ func (j * JSONSchema [T ]) GetSchemaRegistry () SchemaRegistry {
322+ if j == nil {
323+ return nil
324+ }
325+
326+ // Lazily initialize the registry if needed
327+ if j .schemaRegistry == nil {
328+ j .schemaRegistry = NewSchemaRegistry (j .GetDocumentBaseURI ())
329+ }
330+
331+ return j .schemaRegistry
332+ }
333+
334+ // GetDocumentBaseURI returns the base URI for this standalone JSON Schema document.
335+ // This is derived from the $id keyword of the root schema, if present.
336+ // The returned URI is normalized and stripped of any fragment to align with registry behavior.
337+ // This implements the SchemaRegistryProvider interface.
338+ func (j * JSONSchema [T ]) GetDocumentBaseURI () string {
339+ if j == nil {
340+ return ""
341+ }
342+
343+ var uri string
344+
345+ // If we have an explicit document base URI set, use it
346+ if j .documentBaseURI != "" {
347+ uri = j .documentBaseURI
348+ } else if j .IsSchema () && j .GetSchema () != nil {
349+ // Try to get from the root schema's $id
350+ uri = j .GetSchema ().GetID ()
351+ }
352+
353+ if uri == "" {
354+ return ""
355+ }
356+
357+ // Strip fragment and normalize to align with registry behavior
358+ // Per JSON Schema spec, $id should not contain fragments, but we strip for robustness
359+ return normalizeDocumentBaseURI (uri )
360+ }
361+
362+ // normalizeDocumentBaseURI strips fragments and normalizes a URI for use as a document base.
363+ func normalizeDocumentBaseURI (uri string ) string {
364+ if uri == "" {
365+ return ""
366+ }
367+
368+ // Strip fragment if present
369+ if idx := strings .Index (uri , "#" ); idx != - 1 {
370+ uri = uri [:idx ]
371+ }
372+
373+ // Use the same normalization as the registry
374+ return normalizeURI (uri )
375+ }
376+
377+ // SetSchemaRegistry sets the schema registry for this document.
378+ // This is primarily used during unmarshalling to set a pre-created registry.
379+ func (j * JSONSchema [T ]) SetSchemaRegistry (registry SchemaRegistry ) {
380+ if j == nil {
381+ return
382+ }
383+ j .schemaRegistry = registry
384+ }
385+
386+ // SetDocumentBaseURI sets the document base URI for this standalone JSON Schema.
387+ func (j * JSONSchema [T ]) SetDocumentBaseURI (uri string ) {
388+ if j == nil {
389+ return
390+ }
391+ j .documentBaseURI = uri
392+ }
393+
394+ // GetEnclosingSchema returns the Schema that contains this JSONSchema as a field.
395+ // This is used during population to access the parent schema's effective base URI
396+ // for relative $id resolution. Returns nil if not set.
397+ func (j * JSONSchema [T ]) GetEnclosingSchema () * Schema {
398+ if j == nil {
399+ return nil
400+ }
401+ return j .enclosingSchema
402+ }
403+
404+ // SetEnclosingSchema sets the Schema that contains this JSONSchema as a field.
405+ // This is used during population to establish parent-child relationships
406+ // for effective base URI computation.
407+ func (j * JSONSchema [T ]) SetEnclosingSchema (schema * Schema ) {
408+ if j == nil {
409+ return
410+ }
411+ j .enclosingSchema = schema
412+ }
413+
414+ // PopulateWithContext implements the ContextAwarePopulator interface for full context-aware population.
415+ // This method receives the owning document and propagates it to the contained Schema.
416+ func (j * JSONSchema [T ]) PopulateWithContext (source any , ctx * marshaller.PopulationContext ) error {
417+ // If we have a parent that is a Schema, store it as enclosingSchema.
418+ // This is critical for relative $id resolution - children need to access parent's effective base URI.
419+ //
420+ // Note: We only set enclosingSchema here (document tree relationship).
421+ // The parent/topLevelParent fields are for reference chains and are set during
422+ // reference resolution, not population. See documentation on those fields (lines 28-41).
423+ if ctx != nil && ctx .Parent != nil {
424+ if parentSchema , ok := ctx .Parent .(* Schema ); ok {
425+ j .enclosingSchema = parentSchema
310426 }
311427 }
312428
313- // First, perform the standard population
314- if err := j .EitherValue .PopulateWithParent (source , j ); err != nil {
429+ // Determine the owning document for context propagation
430+ // If we're not nested in any other document, this JSONSchema becomes its own owning document
431+ var owningDoc any
432+ if ctx != nil && ctx .OwningDocument != nil {
433+ owningDoc = ctx .OwningDocument
434+ } else {
435+ // This is a standalone JSON Schema document - use itself as the owning document
436+ owningDoc = j
437+ }
438+
439+ // Create a new context with this JSONSchema as the parent for nested schemas
440+ // Note: The Schema's PopulateWithContext will use 'j' (this JSONSchema) as its parent reference
441+ childCtx := & marshaller.PopulationContext {
442+ Parent : j ,
443+ OwningDocument : owningDoc ,
444+ }
445+
446+ // Perform the standard population with context through the EitherValue
447+ if err := j .EitherValue .PopulateWithContext (source , childCtx ); err != nil {
315448 return err
316449 }
317450
0 commit comments