@@ -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,207 @@ 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 = countModelReferences (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 times each model is referenced - both as a oneOf member and as a
302+ * property type. A child can only be inlined if it's referenced exactly once (by its
303+ * oneOf parent) and not used as a property type elsewhere.
304+ */
305+ private Map <String , Integer > countModelReferences (Map <String , CodegenModel > allModels ) {
306+ Map <String , Integer > counts = new HashMap <>();
307+
308+ // Count oneOf parent references
309+ allModels .values ().stream ()
310+ .flatMap (m -> m .oneOf .stream ())
311+ .forEach (name -> counts .merge (name , 1 , Integer ::sum ));
312+
313+ // Count property-type references (prevents inlining models used as field types).
314+ // Check both dataType and complexType
315+ allModels .values ().stream ()
316+ .flatMap (m -> m .vars .stream ())
317+ .forEach (prop -> {
318+ if (prop .dataType != null && allModels .containsKey (prop .dataType )) {
319+ counts .merge (prop .dataType , 1 , Integer ::sum );
320+ }
321+ if (prop .complexType != null && allModels .containsKey (prop .complexType )) {
322+ counts .merge (prop .complexType , 1 , Integer ::sum );
323+ }
324+ });
325+
326+ return counts ;
327+ }
328+
329+ /**
330+ * Mark oneOf parents as sealed/regular traits with discriminator vendor extensions,
331+ * and configure child models for inlining.
332+ */
333+ private void markOneOfTraits (
334+ Map <String , ModelsMap > modelsMap ,
335+ Map <String , CodegenModel > allModels ,
336+ Map <String , Integer > refCounts ) {
337+ for (ModelsMap mm : modelsMap .values ()) {
338+ for (ModelMap modelMap : mm .getModels ()) {
339+ CodegenModel model = modelMap .getModel ();
340+
341+ if (!model .oneOf .isEmpty ()) {
342+ configureOneOfModel (model , allModels , refCounts );
343+ }
344+
345+ if (model .discriminator != null ) {
346+ model .getVendorExtensions ().put ("x-use-discr" , true );
347+ if (model .discriminator .getMapping () != null ) {
348+ model .getVendorExtensions ().put ("x-use-discr-mapping" , true );
349+ }
350+ }
351+ }
352+ }
353+ }
354+
355+ private void configureOneOfModel (
356+ CodegenModel parent ,
357+ Map <String , CodegenModel > allModels ,
358+ Map <String , Integer > refCounts ) {
359+ List <CodegenModel > inlineableMembers = new ArrayList <>();
360+ Set <String > childImports = new HashSet <>();
361+
362+ for (String childName : parent .oneOf ) {
363+ CodegenModel child = allModels .get (childName );
364+ if (child == null ) continue ;
365+
366+ // All children extend the parent trait
367+ child .getVendorExtensions ().put ("x-oneOfParent" , parent .classname );
368+ if (parent .discriminator != null ) {
369+ child .getVendorExtensions ().put ("x-parentDiscriminatorName" ,
370+ parent .discriminator .getPropertyName ());
371+ }
372+
373+ if (isInlineable (child , refCounts )) {
374+ child .getVendorExtensions ().put ("x-isOneOfMember" , true );
375+ inlineableMembers .add (child );
376+ if (child .imports != null ) {
377+ childImports .addAll (child .imports );
378+ }
379+ }
380+ }
381+
382+ buildDiscriminatorEntries (parent , allModels );
383+
384+ if (!inlineableMembers .isEmpty () && inlineableMembers .size () == parent .oneOf .size ()) {
385+ markAsSealedTrait (parent , inlineableMembers , childImports );
386+ } else {
387+ markAsRegularTrait (parent , inlineableMembers );
388+ }
389+ }
390+
391+ private boolean isInlineable (CodegenModel child , Map <String , Integer > refCounts ) {
392+ return (child .oneOf == null || child .oneOf .isEmpty ())
393+ && refCounts .getOrDefault (child .classname , 0 ) == 1 ;
394+ }
395+
396+ private void buildDiscriminatorEntries (CodegenModel parent , Map <String , CodegenModel > allModels ) {
397+ List <Map <String , String >> entries = parent .oneOf .stream ()
398+ .map (allModels ::get )
399+ .filter (Objects ::nonNull )
400+ .map (child -> Map .of ("classname" , child .classname , "schemaName" , child .name ))
401+ .collect (java .util .stream .Collectors .toList ());
402+ parent .getVendorExtensions ().put ("x-discriminator-entries" , entries );
403+ }
404+
405+ private void markAsSealedTrait (
406+ CodegenModel parent ,
407+ List <CodegenModel > members ,
408+ Set <String > childImports ) {
409+ parent .getVendorExtensions ().put ("x-isSealedTrait" , true );
410+ parent .getVendorExtensions ().put ("x-oneOfMembers" , members );
411+
412+ if (parent .getVendorExtensions ().containsKey ("x-synthesized-oneOf" )
413+ && parent .vars != null && !parent .vars .isEmpty ()) {
414+ parent .getVendorExtensions ().put ("x-hasOwnVars" , true );
415+ }
416+
417+ mergeChildImports (parent , childImports );
418+ }
419+
420+ private void markAsRegularTrait (CodegenModel parent , List <CodegenModel > partialMembers ) {
421+ parent .getVendorExtensions ().put ("x-isRegularTrait" , true );
422+ for (CodegenModel member : partialMembers ) {
423+ member .getVendorExtensions ().remove ("x-isOneOfMember" );
424+ }
425+ }
426+
427+ private void mergeChildImports (CodegenModel parent , Set <String > childImports ) {
428+ if (childImports .isEmpty ()) return ;
429+ Set <String > existing = parent .imports != null ? new HashSet <>(parent .imports ) : new HashSet <>();
430+ childImports .removeAll (existing );
431+ if (!childImports .isEmpty ()) {
432+ if (parent .imports == null ) {
433+ parent .imports = new HashSet <>();
434+ }
435+ parent .imports .addAll (childImports );
436+ }
437+ }
438+
439+ /**
440+ * Remove models that were inlined into their parent sealed trait -
441+ * they don't need separate files.
442+ */
443+ private void removeInlinedModels (Map <String , ModelsMap > modelsMap ) {
444+ modelsMap .entrySet ().removeIf (entry ->
445+ entry .getValue ().getModels ().stream ()
446+ .anyMatch (m -> m .getModel ().getVendorExtensions ().containsKey ("x-isOneOfMember" ))
447+ );
246448 }
247449
248450 /**
0 commit comments