@@ -5,7 +5,13 @@ import {
55import c from "ansi-colors" ;
66import supportsColor from "supports-color" ;
77import ts from "typescript" ;
8- import type { DiscriminatorObject , OpenAPI3 } from "../types.js" ;
8+ import type {
9+ DiscriminatorObject ,
10+ OpenAPI3 ,
11+ OpenAPITSOptions ,
12+ ReferenceObject ,
13+ SchemaObject ,
14+ } from "../types.js" ;
915import { tsLiteral , tsModifiers , tsPropertyIndex } from "./ts.js" ;
1016
1117if ( ! supportsColor . stdout || supportsColor . stdout . hasBasic === false ) {
@@ -165,15 +171,154 @@ export function resolveRef<T>(
165171 return node ;
166172}
167173
174+ function createDiscriminatorEnum (
175+ values : string [ ] ,
176+ prevSchema ?: SchemaObject ,
177+ ) : SchemaObject {
178+ return {
179+ type : "string" ,
180+ enum : values ,
181+ description : prevSchema ?. description
182+ ? `${ prevSchema . description } (enum property replaced by openapi-typescript)`
183+ : `discriminator enum property added by openapi-typescript` ,
184+ } ;
185+ }
186+
187+ type InternalDiscriminatorMapping = Record <
188+ string ,
189+ { inferred ?: string ; defined ?: string [ ] }
190+ > ;
191+
168192/** Return a key–value map of discriminator objects found in a schema */
169- export function scanDiscriminators ( schema : OpenAPI3 ) {
170- const discriminators : Record < string , DiscriminatorObject > = { } ;
193+ export function scanDiscriminators (
194+ schema : OpenAPI3 ,
195+ options : OpenAPITSOptions ,
196+ ) {
197+ // all discriminator objects found in the schema
198+ const objects : Record < string , DiscriminatorObject > = { } ;
199+
200+ // refs of all mapped schema objects we have successfully handled to infer the discriminator enum value
201+ const refsHandled : string [ ] = [ ] ;
171202
172- // perform 2 passes: first, collect all discriminator definitions
203+ // perform 2 passes: first, collect all discriminator definitions and handle oneOf and mappings
173204 walk ( schema , ( obj , path ) => {
174- if ( ( obj ?. discriminator as DiscriminatorObject ) ?. propertyName ) {
175- discriminators [ createRef ( path ) ] =
176- obj . discriminator as DiscriminatorObject ;
205+ const discriminator = obj ?. discriminator as DiscriminatorObject | undefined ;
206+ if ( ! discriminator ?. propertyName ) {
207+ return ;
208+ }
209+
210+ // collect discriminator object for later usage
211+ const ref = createRef ( path ) ;
212+
213+ objects [ ref ] = discriminator ;
214+
215+ // if a mapping is available we will help Typescript to infer properties by adding the discriminator enum with its single mapped value to each schema
216+ // we only handle the mapping in advance for discriminator + oneOf compositions right now
217+ if ( ! obj ?. oneOf || ! Array . isArray ( obj . oneOf ) ) {
218+ return ;
219+ }
220+
221+ const oneOf : ( SchemaObject | ReferenceObject ) [ ] = obj . oneOf ;
222+ const mapping : InternalDiscriminatorMapping = { } ;
223+
224+ // the mapping can be inferred from the oneOf refs next to the discriminator object
225+ for ( const item of oneOf ) {
226+ if ( "$ref" in item ) {
227+ // the name of the schema is the inferred discriminator enum value
228+ const value = item . $ref . split ( "/" ) . pop ( ) ;
229+
230+ if ( value ) {
231+ if ( ! mapping [ item . $ref ] ) {
232+ mapping [ item . $ref ] = { inferred : value } ;
233+ } else {
234+ mapping [ item . $ref ] . inferred = value ;
235+ }
236+ }
237+ }
238+ }
239+
240+ // the mapping can be defined in the discriminator object itself
241+ if ( discriminator . mapping ) {
242+ for ( const mappedValue in discriminator . mapping ) {
243+ const mappedRef = discriminator . mapping [ mappedValue ] ;
244+ if ( ! mappedRef ) {
245+ continue ;
246+ }
247+
248+ if ( ! mapping [ mappedRef ] ?. defined ) {
249+ // this overrides inferred values, but we don't need them anymore as soon as we have a defined value
250+ mapping [ mappedRef ] = { defined : [ ] } ;
251+ }
252+
253+ mapping [ mappedRef ] . defined ?. push ( mappedValue ) ;
254+ }
255+ }
256+
257+ for ( const [ mappedRef , { inferred, defined } ] of Object . entries ( mapping ) ) {
258+ if ( refsHandled . includes ( mappedRef ) ) {
259+ continue ;
260+ }
261+
262+ if ( ! inferred && ! defined ) {
263+ continue ;
264+ }
265+
266+ // prefer defined values over automatically inferred ones
267+ // the inferred enum values from the schema might not represent the actual enum values of the discriminator,
268+ // so if we have defined values, use them instead
269+ const mappedValues = defined ?? [ inferred ! ] ;
270+ const resolvedSchema = resolveRef < SchemaObject > ( schema , mappedRef , {
271+ silent : options . silent ?? false ,
272+ } ) ;
273+
274+ if ( resolvedSchema ?. allOf ) {
275+ // if the schema is an allOf, we can append a new schema object to the allOf array
276+ resolvedSchema . allOf . push ( {
277+ type : "object" ,
278+ // discriminator enum properties always need to be required
279+ required : [ discriminator . propertyName ] ,
280+ properties : {
281+ [ discriminator . propertyName ] : createDiscriminatorEnum ( mappedValues ) ,
282+ } ,
283+ } ) ;
284+
285+ refsHandled . push ( mappedRef ) ;
286+ } else if (
287+ typeof resolvedSchema === "object" &&
288+ "type" in resolvedSchema &&
289+ resolvedSchema . type === "object"
290+ ) {
291+ // if the schema is an object, we can apply the discriminator enums to its properties
292+ if ( ! resolvedSchema . properties ) {
293+ resolvedSchema . properties = { } ;
294+ }
295+
296+ // discriminator enum properties always need to be required
297+ if ( ! resolvedSchema . required ) {
298+ resolvedSchema . required = [ discriminator . propertyName ] ;
299+ } else if (
300+ ! resolvedSchema . required . includes ( discriminator . propertyName )
301+ ) {
302+ resolvedSchema . required . push ( discriminator . propertyName ) ;
303+ }
304+
305+ // add/replace the discriminator enum property
306+ resolvedSchema . properties [ discriminator . propertyName ] =
307+ createDiscriminatorEnum (
308+ mappedValues ,
309+ resolvedSchema . properties [ discriminator . propertyName ] ,
310+ ) ;
311+
312+ refsHandled . push ( mappedRef ) ;
313+ } else {
314+ warn (
315+ `Discriminator mapping has an invalid schema (neither an object schema nor an allOf array): ${ mappedRef } => ${ mappedValues . join (
316+ ", " ,
317+ ) } (Discriminator: ${ ref } )`,
318+ options . silent ,
319+ ) ;
320+ continue ;
321+ }
177322 }
178323 } ) ;
179324
@@ -185,19 +330,20 @@ export function scanDiscriminators(schema: OpenAPI3) {
185330 if ( obj && Array . isArray ( obj [ key ] ) ) {
186331 for ( const item of ( obj as any ) [ key ] ) {
187332 if ( "$ref" in item ) {
188- if ( discriminators [ item . $ref ] ) {
189- discriminators [ createRef ( path ) ] = {
190- ...discriminators [ item . $ref ] ,
333+ if ( objects [ item . $ref ] ) {
334+ objects [ createRef ( path ) ] = {
335+ ...objects [ item . $ref ] ,
191336 } ;
192337 }
193338 } else if ( item . discriminator ?. propertyName ) {
194- discriminators [ createRef ( path ) ] = { ...item . discriminator } ;
339+ objects [ createRef ( path ) ] = { ...item . discriminator } ;
195340 }
196341 }
197342 }
198343 }
199344 } ) ;
200- return discriminators ;
345+
346+ return { objects, refsHandled } ;
201347}
202348
203349/** Walk through any JSON-serializable (i.e. non-circular) object */
0 commit comments