@@ -94,6 +94,10 @@ public ScalaSttpClientCodegen() {
9494 .excludeSchemaSupportFeatures (
9595 SchemaSupportFeature .Polymorphism
9696 )
97+ .includeSchemaSupportFeatures (
98+ SchemaSupportFeature .oneOf ,
99+ SchemaSupportFeature .allOf
100+ )
97101 .excludeParameterFeatures (
98102 ParameterFeature .Cookie
99103 )
@@ -240,9 +244,186 @@ public ModelsMap postProcessModels(ModelsMap objs) {
240244 */
241245 @ Override
242246 public Map <String , ModelsMap > postProcessAllModels (Map <String , ModelsMap > objs ) {
243- final Map <String , ModelsMap > processed = super .postProcessAllModels (objs );
244- postProcessUpdateImports (processed );
245- return processed ;
247+ Map <String , ModelsMap > modelsMap = super .postProcessAllModels (objs );
248+
249+ Map <String , CodegenModel > allModels = collectAllModels (modelsMap );
250+ synthesizeOneOfFromDiscriminator (allModels );
251+ Map <String , Integer > refCounts = countOneOfReferences (allModels );
252+ markOneOfTraits (modelsMap , allModels , refCounts );
253+ removeInlinedModels (modelsMap );
254+
255+ postProcessUpdateImports (modelsMap );
256+ return modelsMap ;
257+ }
258+
259+ /**
260+ * Collect all CodegenModels by classname for lookup.
261+ */
262+ private Map <String , CodegenModel > collectAllModels (Map <String , ModelsMap > modelsMap ) {
263+ return modelsMap .values ().stream ()
264+ .flatMap (mm -> mm .getModels ().stream ())
265+ .map (ModelMap ::getModel )
266+ .collect (java .util .stream .Collectors .toMap (m -> m .classname , m -> m , (a , b ) -> a ));
267+ }
268+
269+ /**
270+ * For specs that use allOf+discriminator (children reference parent via allOf, parent has
271+ * discriminator.mapping but no oneOf), synthesize the oneOf set from the discriminator mapping.
272+ * This allows the standard oneOf processing logic to handle both patterns uniformly.
273+ */
274+ private void synthesizeOneOfFromDiscriminator (Map <String , CodegenModel > allModels ) {
275+ for (CodegenModel model : allModels .values ()) {
276+ if (!model .oneOf .isEmpty () || model .discriminator == null ) {
277+ continue ;
278+ }
279+
280+ if (model .discriminator .getMappedModels () != null
281+ && !model .discriminator .getMappedModels ().isEmpty ()) {
282+ for (CodegenDiscriminator .MappedModel mapped : model .discriminator .getMappedModels ()) {
283+ model .oneOf .add (mapped .getModelName ());
284+ }
285+ } else if (model .discriminator .getMapping () != null ) {
286+ for (String ref : model .discriminator .getMapping ().values ()) {
287+ String modelName = ref .contains ("/" ) ? ref .substring (ref .lastIndexOf ('/' ) + 1 ) : ref ;
288+ if (allModels .containsKey (modelName )) {
289+ model .oneOf .add (modelName );
290+ }
291+ }
292+ }
293+
294+ if (!model .oneOf .isEmpty ()) {
295+ model .getVendorExtensions ().put ("x-synthesized-oneOf" , true );
296+ }
297+ }
298+ }
299+
300+ /**
301+ * Count how many oneOf parents reference each child, used to determine
302+ * whether a child can be inlined (only if referenced by exactly one parent).
303+ */
304+ private Map <String , Integer > countOneOfReferences (Map <String , CodegenModel > allModels ) {
305+ return allModels .values ().stream ()
306+ .flatMap (m -> m .oneOf .stream ())
307+ .collect (java .util .stream .Collectors .toMap (name -> name , name -> 1 , Integer ::sum ));
308+ }
309+
310+ /**
311+ * Mark oneOf parents as sealed/regular traits with discriminator vendor extensions,
312+ * and configure child models for inlining.
313+ */
314+ private void markOneOfTraits (Map <String , ModelsMap > modelsMap ,
315+ Map <String , CodegenModel > allModels ,
316+ Map <String , Integer > refCounts ) {
317+ for (ModelsMap mm : modelsMap .values ()) {
318+ for (ModelMap modelMap : mm .getModels ()) {
319+ CodegenModel model = modelMap .getModel ();
320+
321+ if (!model .oneOf .isEmpty ()) {
322+ configureOneOfModel (model , allModels , refCounts );
323+ }
324+
325+ if (model .discriminator != null ) {
326+ model .getVendorExtensions ().put ("x-use-discr" , true );
327+ if (model .discriminator .getMapping () != null ) {
328+ model .getVendorExtensions ().put ("x-use-discr-mapping" , true );
329+ }
330+ }
331+ }
332+ }
333+ }
334+
335+ private void configureOneOfModel (CodegenModel parent ,
336+ Map <String , CodegenModel > allModels ,
337+ Map <String , Integer > refCounts ) {
338+ List <CodegenModel > inlineableMembers = new ArrayList <>();
339+ Set <String > childImports = new HashSet <>();
340+
341+ for (String childName : parent .oneOf ) {
342+ CodegenModel child = allModels .get (childName );
343+ if (child != null && isInlineable (child , refCounts )) {
344+ markChildForInlining (child , parent );
345+ inlineableMembers .add (child );
346+ if (child .imports != null ) {
347+ childImports .addAll (child .imports );
348+ }
349+ }
350+ }
351+
352+ buildDiscriminatorEntries (parent , allModels );
353+
354+ if (!inlineableMembers .isEmpty () && inlineableMembers .size () == parent .oneOf .size ()) {
355+ markAsSealedTrait (parent , inlineableMembers , childImports );
356+ } else {
357+ markAsRegularTrait (parent , inlineableMembers );
358+ }
359+ }
360+
361+ private boolean isInlineable (CodegenModel child , Map <String , Integer > refCounts ) {
362+ return (child .oneOf == null || child .oneOf .isEmpty ())
363+ && refCounts .getOrDefault (child .classname , 0 ) == 1 ;
364+ }
365+
366+ private void markChildForInlining (CodegenModel child , CodegenModel parent ) {
367+ child .getVendorExtensions ().put ("x-isOneOfMember" , true );
368+ child .getVendorExtensions ().put ("x-oneOfParent" , parent .classname );
369+ if (parent .discriminator != null ) {
370+ child .getVendorExtensions ().put ("x-parentDiscriminatorName" ,
371+ parent .discriminator .getPropertyName ());
372+ }
373+ }
374+
375+ private void buildDiscriminatorEntries (CodegenModel parent , Map <String , CodegenModel > allModels ) {
376+ List <Map <String , String >> entries = parent .oneOf .stream ()
377+ .map (allModels ::get )
378+ .filter (Objects ::nonNull )
379+ .map (child -> Map .of ("classname" , child .classname , "schemaName" , child .name ))
380+ .collect (java .util .stream .Collectors .toList ());
381+ parent .getVendorExtensions ().put ("x-discriminator-entries" , entries );
382+ }
383+
384+ private void markAsSealedTrait (CodegenModel parent , List <CodegenModel > members ,
385+ Set <String > childImports ) {
386+ parent .getVendorExtensions ().put ("x-isSealedTrait" , true );
387+ parent .getVendorExtensions ().put ("x-oneOfMembers" , members );
388+
389+ if (parent .getVendorExtensions ().containsKey ("x-synthesized-oneOf" )
390+ && parent .vars != null && !parent .vars .isEmpty ()) {
391+ parent .getVendorExtensions ().put ("x-hasOwnVars" , true );
392+ }
393+
394+ mergeChildImports (parent , childImports );
395+ }
396+
397+ private void markAsRegularTrait (CodegenModel parent , List <CodegenModel > partialMembers ) {
398+ parent .getVendorExtensions ().put ("x-isRegularTrait" , true );
399+ for (CodegenModel member : partialMembers ) {
400+ member .getVendorExtensions ().remove ("x-isOneOfMember" );
401+ member .getVendorExtensions ().remove ("x-oneOfParent" );
402+ member .getVendorExtensions ().remove ("x-parentDiscriminatorName" );
403+ }
404+ }
405+
406+ private void mergeChildImports (CodegenModel parent , Set <String > childImports ) {
407+ if (childImports .isEmpty ()) return ;
408+ Set <String > existing = parent .imports != null ? new HashSet <>(parent .imports ) : new HashSet <>();
409+ childImports .removeAll (existing );
410+ if (!childImports .isEmpty ()) {
411+ if (parent .imports == null ) {
412+ parent .imports = new HashSet <>();
413+ }
414+ parent .imports .addAll (childImports );
415+ }
416+ }
417+
418+ /**
419+ * Remove models that were inlined into their parent sealed trait -
420+ * they don't need separate files.
421+ */
422+ private void removeInlinedModels (Map <String , ModelsMap > modelsMap ) {
423+ modelsMap .entrySet ().removeIf (entry ->
424+ entry .getValue ().getModels ().stream ()
425+ .anyMatch (m -> m .getModel ().getVendorExtensions ().containsKey ("x-isOneOfMember" ))
426+ );
246427 }
247428
248429 /**
0 commit comments