@@ -86,15 +86,17 @@ public ScalaSttp4ClientCodegen() {
8686 )
8787 );
8888
89+ // Enable oneOf interface generation
90+ useOneOfInterfaces = true ;
91+ supportsMultipleInheritance = true ;
92+ supportsInheritance = true ;
93+ addOneOfInterfaceImports = true ;
94+
8995 outputFolder = "generated-code/scala-sttp4" ;
9096 modelTemplateFiles .put ("model.mustache" , ".scala" );
9197 apiTemplateFiles .put ("api.mustache" , ".scala" );
9298 embeddedTemplateDir = templateDir = "scala-sttp4" ;
9399
94- String jsonLibrary = JSON_LIBRARY_PROPERTY .getValue (additionalProperties );
95-
96- String jsonValueClass = "circe" .equals (jsonLibrary ) ? "io.circe.Json" : "org.json4s.JValue" ;
97-
98100 additionalProperties .put (CodegenConstants .GROUP_ID , groupId );
99101 additionalProperties .put (CodegenConstants .ARTIFACT_ID , artifactId );
100102 additionalProperties .put (CodegenConstants .ARTIFACT_VERSION , artifactVersion );
@@ -124,13 +126,12 @@ public ScalaSttp4ClientCodegen() {
124126 typeMapping .put ("short" , "Short" );
125127 typeMapping .put ("char" , "Char" );
126128 typeMapping .put ("double" , "Double" );
127- typeMapping .put ("object" , "Any" );
128129 typeMapping .put ("file" , "File" );
129130 typeMapping .put ("binary" , "File" );
130131 typeMapping .put ("number" , "Double" );
131132 typeMapping .put ("decimal" , "BigDecimal" );
132133 typeMapping .put ("ByteArray" , "Array[Byte]" );
133- typeMapping . put ( " AnyType" , jsonValueClass );
134+ // AnyType and object mapping will be set in processOpts() based on jsonLibrary
134135
135136 instantiationTypes .put ("array" , "ListBuffer" );
136137 instantiationTypes .put ("map" , "Map" );
@@ -149,6 +150,20 @@ public void processOpts() {
149150 apiPackage = PACKAGE_PROPERTY .getApiPackage (additionalProperties );
150151 modelPackage = PACKAGE_PROPERTY .getModelPackage (additionalProperties );
151152
153+ // Set AnyType and object mapping based on jsonLibrary
154+ String jsonLibrary = JSON_LIBRARY_PROPERTY .getValue (additionalProperties );
155+ if ("circe" .equals (jsonLibrary )) {
156+ typeMapping .put ("AnyType" , "io.circe.Json" );
157+ typeMapping .put ("object" , "io.circe.JsonObject" );
158+ importMapping .put ("io.circe.Json" , "io.circe.Json" );
159+ importMapping .put ("io.circe.JsonObject" , "io.circe.JsonObject" );
160+ } else {
161+ typeMapping .put ("AnyType" , "org.json4s.JValue" );
162+ typeMapping .put ("object" , "org.json4s.JObject" );
163+ importMapping .put ("org.json4s.JValue" , "org.json4s.JValue" );
164+ importMapping .put ("org.json4s.JObject" , "org.json4s.JObject" );
165+ }
166+
152167 supportingFiles .add (new SupportingFile ("README.mustache" , "" , "README.md" ));
153168 supportingFiles .add (new SupportingFile ("build.sbt.mustache" , "" , "build.sbt" ));
154169 final String invokerFolder = (sourceFolder + File .separator + invokerPackage ).replace ("." , File .separator );
@@ -221,6 +236,87 @@ public ModelsMap postProcessModels(ModelsMap objs) {
221236 @ Override
222237 public Map <String , ModelsMap > postProcessAllModels (Map <String , ModelsMap > objs ) {
223238 final Map <String , ModelsMap > processed = super .postProcessAllModels (objs );
239+
240+ // First pass: count how many oneOf parents each model has
241+ Map <String , Integer > oneOfMemberCount = new HashMap <>();
242+ for (ModelsMap mm : processed .values ()) {
243+ for (ModelMap model : mm .getModels ()) {
244+ CodegenModel cModel = model .getModel ();
245+ if (!cModel .oneOf .isEmpty ()) {
246+ for (String childName : cModel .oneOf ) {
247+ oneOfMemberCount .put (childName , oneOfMemberCount .getOrDefault (childName , 0 ) + 1 );
248+ }
249+ }
250+ }
251+ }
252+
253+ // Second pass: process models
254+ for (ModelsMap mm : processed .values ()) {
255+ for (ModelMap model : mm .getModels ()) {
256+ CodegenModel cModel = model .getModel ();
257+
258+ if (!cModel .oneOf .isEmpty ()) {
259+ cModel .getVendorExtensions ().put ("x-isSealedTrait" , true );
260+
261+ // Collect child models for inline generation
262+ // Only inline if they are used exclusively by this oneOf parent
263+ List <CodegenModel > childModels = new ArrayList <>();
264+
265+ for (String childName : cModel .oneOf ) {
266+ CodegenModel childModel = ModelUtils .getModelByName (childName , processed );
267+ if (childModel != null && oneOfMemberCount .getOrDefault (childName , 0 ) == 1 ) {
268+ // This child is only used by this parent - can be inlined
269+ childModel .getVendorExtensions ().put ("x-isOneOfMember" , true );
270+ childModel .getVendorExtensions ().put ("x-oneOfParent" , cModel .classname );
271+
272+ // Remove discriminator field from child if parent has discriminator
273+ // (circe-generic-extras adds it automatically)
274+ if (cModel .discriminator != null ) {
275+ String discriminatorName = cModel .discriminator .getPropertyName ();
276+ childModel .vars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
277+ childModel .allVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
278+ childModel .requiredVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
279+ childModel .optionalVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
280+ }
281+
282+ childModels .add (childModel );
283+ }
284+ }
285+ cModel .getVendorExtensions ().put ("x-oneOfMembers" , childModels );
286+ } else if (cModel .isEnum ) {
287+ cModel .getVendorExtensions ().put ("x-isEnum" , true );
288+ } else {
289+ cModel .getVendorExtensions ().put ("x-isRegularModel" , true );
290+ }
291+
292+ if (cModel .discriminator != null ) {
293+ cModel .getVendorExtensions ().put ("x-use-discr" , true );
294+
295+ if (cModel .discriminator .getMapping () != null ) {
296+ cModel .getVendorExtensions ().put ("x-use-discr-mapping" , true );
297+ }
298+ }
299+
300+ // Remove discriminator property from models that extend a oneOf parent
301+ // (circe-generic-extras adds it automatically)
302+ if (cModel .parent != null && cModel .parentModel != null && cModel .parentModel .discriminator != null ) {
303+ String discriminatorName = cModel .parentModel .discriminator .getPropertyName ();
304+ cModel .vars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
305+ cModel .allVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
306+ cModel .requiredVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
307+ cModel .optionalVars .removeIf (prop -> prop .baseName .equals (discriminatorName ));
308+ }
309+ }
310+ }
311+
312+ // Third pass: remove oneOf members from the map to skip file generation
313+ // (they are already inlined in their parent sealed trait)
314+ processed .entrySet ().removeIf (entry -> {
315+ ModelsMap mm = entry .getValue ();
316+ return mm .getModels ().stream ()
317+ .anyMatch (model -> model .getModel ().getVendorExtensions ().containsKey ("x-isOneOfMember" ));
318+ });
319+
224320 postProcessUpdateImports (processed );
225321 return processed ;
226322 }
0 commit comments