@@ -60,21 +60,44 @@ function renderTypeConstraints(schema: JsonSchema): string {
6060 return parts . join ( ', ' ) || 'object'
6161}
6262
63+ // When a titled object type has already been fully expanded once in a schema,
64+ // later occurrences are rendered as a short "(see above)" reference instead of
65+ // being re-expanded. Large REST response schemas reuse shared types (e.g.
66+ // `Simple User` recurs dozens of times), so this keeps output compact and fast
67+ // to render. Returns true if the caller should emit a reference; marks the
68+ // title as seen when it is about to be expanded for the first time.
69+ function shouldReference (
70+ title : string | undefined ,
71+ canExpand : boolean ,
72+ seen : Set < string > ,
73+ ) : boolean {
74+ if ( ! canExpand || ! title ) return false
75+ if ( seen . has ( title ) ) return true
76+ seen . add ( title )
77+ return false
78+ }
79+
6380function renderCompositionVariants (
6481 keyword : string ,
6582 variants : JsonSchema [ ] ,
6683 indent : number ,
6784 depth : number ,
85+ seen : Set < string > ,
6886) : string {
6987 const prefix = ' ' . repeat ( indent )
7088 const label = keyword . replace ( 'Of' , ' of' )
7189 const lines : string [ ] = [ `${ prefix } * ${ label } :` ]
7290
7391 for ( const variant of variants ) {
7492 const name = variant . title || renderTypeConstraints ( variant )
93+ const canExpand = depth < MAX_DEPTH && Boolean ( variant . properties )
94+ if ( shouldReference ( variant . title , canExpand , seen ) ) {
95+ lines . push ( `${ prefix } * **${ name } ** (see above)` )
96+ continue
97+ }
7598 lines . push ( `${ prefix } * **${ name } **` )
76- if ( depth < MAX_DEPTH && variant . properties ) {
77- const nested = renderProperties ( variant , indent + 2 , depth + 1 )
99+ if ( canExpand ) {
100+ const nested = renderProperties ( variant , indent + 2 , depth + 1 , seen )
78101 if ( nested ) lines . push ( nested )
79102 }
80103 }
@@ -86,6 +109,7 @@ function renderProperties(
86109 schema : JsonSchema ,
87110 indent : number ,
88111 depth : number ,
112+ seen : Set < string > ,
89113 requiredFields ?: string [ ] ,
90114) : string {
91115 const props = schema . properties || { }
@@ -103,9 +127,14 @@ function renderProperties(
103127 lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ label } :` )
104128 for ( const variant of prop [ compositionKey ] ! ) {
105129 const vName = variant . title || renderTypeConstraints ( variant )
130+ const canExpand = depth < MAX_DEPTH && Boolean ( variant . properties )
131+ if ( shouldReference ( variant . title , canExpand , seen ) ) {
132+ lines . push ( `${ prefix } * **${ vName } ** (see above)` )
133+ continue
134+ }
106135 lines . push ( `${ prefix } * **${ vName } **` )
107- if ( depth < MAX_DEPTH && variant . properties ) {
108- const nested = renderProperties ( variant , indent + 2 , depth + 1 )
136+ if ( canExpand ) {
137+ const nested = renderProperties ( variant , indent + 2 , depth + 1 , seen )
109138 if ( nested ) lines . push ( nested )
110139 }
111140 }
@@ -118,12 +147,17 @@ function renderProperties(
118147
119148 if ( propType === 'array' && prop . items ) {
120149 const itemTitle = prop . items . title
121- if ( prop . items . properties && depth < MAX_DEPTH ) {
150+ const canExpand = Boolean ( prop . items . properties ) && depth < MAX_DEPTH
151+ if ( shouldReference ( itemTitle , canExpand , seen ) ) {
152+ lines . push (
153+ `${ prefix } * \`${ name } \`: ${ reqStr } array of \`${ itemTitle } \`${ isNullable ? ' or null' : '' } (see above)` ,
154+ )
155+ } else if ( canExpand ) {
122156 const label = itemTitle
123157 ? `array of \`${ itemTitle } \`${ isNullable ? ' or null' : '' } `
124158 : `array of objects${ isNullable ? ' or null' : '' } `
125159 lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ label } :` )
126- lines . push ( renderProperties ( prop . items , indent + 1 , depth + 1 ) )
160+ lines . push ( renderProperties ( prop . items , indent + 1 , depth + 1 , seen ) )
127161 } else {
128162 lines . push (
129163 `${ prefix } * \`${ name } \`: ${ reqStr } array of ${ renderTypeConstraints ( prop . items ) } ${ isNullable ? ' or null' : '' } ` ,
@@ -132,8 +166,12 @@ function renderProperties(
132166 } else if ( prop . properties && depth < MAX_DEPTH ) {
133167 // renderTypeConstraints handles string[] types (e.g. ["object","null"] → "object or null")
134168 const label = prop . title ? `\`${ prop . title } \`` : renderTypeConstraints ( prop )
135- lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ label } :` )
136- lines . push ( renderProperties ( prop , indent + 1 , depth + 1 ) )
169+ if ( shouldReference ( prop . title , true , seen ) ) {
170+ lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ label } (see above)` )
171+ } else {
172+ lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ label } :` )
173+ lines . push ( renderProperties ( prop , indent + 1 , depth + 1 , seen ) )
174+ }
137175 } else {
138176 // renderTypeConstraints handles string[] types (e.g. ["string","null"] → "string or null")
139177 lines . push ( `${ prefix } * \`${ name } \`: ${ reqStr } ${ renderTypeConstraints ( prop ) } ` )
@@ -151,10 +189,15 @@ function renderProperties(
151189export function summarizeSchema ( schema : JsonSchema ) : string {
152190 if ( ! schema || typeof schema !== 'object' ) return ''
153191
192+ // Tracks titled object types already expanded once in this schema. Later
193+ // occurrences are rendered as a "(see above)" reference to keep output
194+ // compact and fast to render (shared types can recur dozens of times).
195+ const seen = new Set < string > ( )
196+
154197 // Handle top-level composition
155198 for ( const keyword of [ 'oneOf' , 'anyOf' , 'allOf' ] as const ) {
156199 if ( schema [ keyword ] ) {
157- return renderCompositionVariants ( keyword , schema [ keyword ] ! , 0 , 0 )
200+ return renderCompositionVariants ( keyword , schema [ keyword ] ! , 0 , 0 , seen )
158201 }
159202 }
160203
@@ -181,9 +224,14 @@ export function summarizeSchema(schema: JsonSchema): string {
181224 const lines = [ `Array${ constraintStr } of ${ titlePart } objects${ nullSuffix } : ${ label } :` ]
182225 for ( const variant of items [ compositionKey ] ! ) {
183226 const name = variant . title || renderTypeConstraints ( variant )
227+ const canExpand = Boolean ( variant . properties )
228+ if ( shouldReference ( variant . title , canExpand , seen ) ) {
229+ lines . push ( ` * **${ name } ** (see above)` )
230+ continue
231+ }
184232 lines . push ( ` * **${ name } **` )
185- if ( variant . properties ) {
186- const nested = renderProperties ( variant , 2 , 1 )
233+ if ( canExpand ) {
234+ const nested = renderProperties ( variant , 2 , 1 , seen )
187235 if ( nested ) lines . push ( nested )
188236 }
189237 }
@@ -193,15 +241,21 @@ export function summarizeSchema(schema: JsonSchema): string {
193241 if ( items . properties ) {
194242 const label = itemTitle ? `\`${ itemTitle } \`` : 'objects'
195243 const nullSuffix = isNullable ? ' or null' : ''
196- return `Array${ constraintStr } of ${ label } ${ nullSuffix } :\n${ renderProperties ( items , 1 , 1 ) } `
244+ if ( itemTitle ) seen . add ( itemTitle )
245+ return `Array${ constraintStr } of ${ label } ${ nullSuffix } :\n${ renderProperties ( items , 1 , 1 , seen ) } `
197246 }
198247
199248 return `Array${ constraintStr } of ${ renderTypeConstraints ( items ) } ${ isNullable ? ' or null' : '' } `
200249 }
201250
202251 // Handle top-level object
203252 if ( schema . properties ) {
204- return renderProperties ( schema , 0 , 0 )
253+ // Note: we deliberately do NOT pre-mark schema.title here. Unlike the
254+ // array-items case above, a top-level object emits no visible titled
255+ // header, so pre-marking would make a self-referential property render a
256+ // dangling "(see above)" pointing at nothing. Letting it expand one
257+ // depth-bounded level is correct and clearer.
258+ return renderProperties ( schema , 0 , 0 , seen )
205259 }
206260
207261 return renderTypeConstraints ( schema )
0 commit comments