Design a robust, deterministic strategy to inline all JSON Schema $ref occurrences into their target schemas for docs JSON produced by goa. The generator only uses local references to the top-level definitions object (paths of the form "#/definitions/").
- User/Result types: Type schemas reference definitions via
TypeRef*andResultTypeRef*. - Object properties: Nested fields can be
$ref. - Array items:
itemsmay be a$refwhen the element type is a user/result type. - Map values:
additionalPropertiesmay be a schema containing$ref(or booleantrue). - Union types:
anyOfis an array of schemas that may contain$ref. - Service-level top schema: APISchema properties refer to
#/definitions/<Service>.
Assumption: Only references to #/definitions/<Name> are present. No external files/URLs.
- Definitions are in a per-root map; keys are type names.
- Definitions can themselves contain nested
$ref. - Must avoid infinite recursion on cyclical definitions.
- Preserve required fields, examples, and order where applicable.
-
Snapshot definitions
- Work on a deep-copied map of the per-root
definitionsto avoid mutating shared state.
- Work on a deep-copied map of the per-root
-
Inlining pass
- Implement
inlineRefs(schema, defs, stack):- If
schema.Ref == "#/definitions/<Name>":- If
<Name>is instack, keep$ref(cycle break) and return. - Else: push
<Name>, deep-copydefs[Name], recursivelyinlineRefson the copy, then replace current node with the copy and clearRef. Pop<Name>.
- If
- If
schema.Ref == "": recursively process all composite positions:propertiesvaluesitemsadditionalPropertieswhen it is a*SchemaanyOfelements- (If present) nested
definitionsfor completeness
- If
- Implement
-
Apply order
- Perform schema transforms that affect keys first (e.g., JSON tag rename and required filtering), then inline.
-
Service-level application
- Run
inlineRefson every reachable schema under services (payload, result, streaming payload/result, error types). - Optionally inline inside
definitionsif you plan to drop them entirely.
- Run
-
Cycles
- Use a
stack(set of definition names being expanded). If a name is already in the stack, retain$refat that edge to prevent infinite expansion. This yields minimal$reffor strongly-cyclic graphs.
- Use a
-
AnyOf
- Goa builds
anyOfby appending union variants in-order. Inline each element; preserve order.
- Goa builds
-
Maps
- If
additionalPropertiesistrue, leave as is. If it is a schema, inline there as well.
- If
-
Performance
- Start without memoization. If profiling reveals hotspots, consider caching fully inlined, deep-copied definitions by name and reusing them, taking care not to share mutable pointers unexpectedly.
-
Post-processing
- If you require a completely
$ref-free document, removedefinitionsafter inlining all reachable schemas. Otherwise, keep or prune unused definitions based on tests/goldens.
- If you require a completely
func inlineAllServiceSchemas(d *data, defs map[string]*openapi.Schema) {
stack := map[string]bool{}
for _, s := range d.Services {
for _, m := range s.Methods {
if m.Payload != nil && m.Payload.Type != nil {
inlineRefs(m.Payload.Type, defs, stack)
}
if m.StreamingPayload != nil && m.StreamingPayload.Type != nil {
inlineRefs(m.StreamingPayload.Type, defs, stack)
}
if m.Result != nil && m.Result.Type != nil {
inlineRefs(m.Result.Type, defs, stack)
}
if m.StreamingResult != nil && m.StreamingResult.Type != nil {
inlineRefs(m.StreamingResult.Type, defs, stack)
}
for _, e := range m.Errors {
if e.Type != nil {
inlineRefs(e.Type, defs, stack)
}
}
}
}
}
func inlineRefs(s *openapi.Schema, defs map[string]*openapi.Schema, stack map[string]bool) {
if s == nil {
return
}
if s.Ref != "" {
const prefix = "#/definitions/"
if !strings.HasPrefix(s.Ref, prefix) { return }
name := strings.TrimPrefix(s.Ref, prefix)
if stack[name] { return }
def, ok := defs[name]
if !ok || def == nil { return }
stack[name] = true
copy := dupSchema(def)
inlineRefs(copy, defs, stack)
*s = *copy
s.Ref = ""
delete(stack, name)
return
}
for _, p := range s.Properties { inlineRefs(p, defs, stack) }
if s.Items != nil { inlineRefs(s.Items, defs, stack) }
if ap, ok := s.AdditionalProperties.(*openapi.Schema); ok && ap != nil {
inlineRefs(ap, defs, stack)
}
for _, a := range s.AnyOf { inlineRefs(a, defs, stack) }
for _, d := range s.Definitions { inlineRefs(d, defs, stack) }
}- After building
docsanddefs, and after JSON tag transforms:- Gate behind
InlineRefsoption: callinlineAllServiceSchemas(docs, defs). - Decide whether to keep or drop
docs.Definitionsbased on goldens. If keeping, you may optionally prune unused definitions.
- Gate behind
- Cycles: Minimal
$refretained on cycle entry. - Required and examples: Preserved; JSON-tag transform already remaps them pre-inlining.
- Maps: Free-form (
true) untouched; schema-valued inlined. - Unions: Order preserved in
anyOf.
- Tailored to the shapes produced by goa: only
#/definitions/refs. - Deterministic and safe (deep copies, cycle guard), compositional (handles all container fields).
- Clean pipeline: rename/tag first, inline second.