From 13e1f3658eb16e4251e2519cc0e4f668d0441b8a Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Tue, 7 Apr 2026 16:08:54 -0700 Subject: [PATCH 1/8] [Fix][scala-sttp][circe] Circe codecs do not preserve original JSON field names for non-camelCase properties --- .../languages/ScalaSttpClientCodegen.java | 2 +- .../additionalTypeSerializers.mustache | 55 ++++-- .../resources/scala-sttp/jsonSupport.mustache | 3 +- .../main/resources/scala-sttp/model.mustache | 29 +++ .../codegen/scala/SttpCodegenTest.java | 67 +++++++ .../3_0/scala/mixed-case-fields.yaml | 30 +++ .../core/AdditionalTypeSerializers.scala | 53 ++++-- .../client/core/JsonSupport.scala | 3 +- .../client/model/ApiResponse.scala | 25 +++ .../openapitools/client/model/Category.scala | 22 +++ .../openapitools/client/model/EnumTest.scala | 25 +++ .../org/openapitools/client/model/Order.scala | 34 ++++ .../org/openapitools/client/model/Pet.scala | 34 ++++ .../client/model/PropertyNameMapping.scala | 28 +++ .../org/openapitools/client/model/Tag.scala | 22 +++ .../org/openapitools/client/model/User.scala | 40 ++++ .../core/AdditionalTypeSerializers.scala | 2 +- samples/yaml/user.yml | 178 ------------------ 18 files changed, 439 insertions(+), 213 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java index 8e5c9ec22df9..bc3a3f191bb7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java @@ -141,7 +141,7 @@ public ScalaSttpClientCodegen() { typeMapping.put("short", "Short"); typeMapping.put("char", "Char"); typeMapping.put("double", "Double"); - typeMapping.put("object", "Any"); + typeMapping.put("object", jsonValueClass); typeMapping.put("file", "File"); typeMapping.put("binary", "File"); typeMapping.put("number", "Double"); diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache index 9a2c169cc853..a8c11eb2a5c7 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache @@ -7,7 +7,7 @@ object AdditionalTypeSerializers { import org.json4s.{Serializer, CustomSerializer, JNull, MappingException} import org.json4s.JsonAST.JString case object URISerializer extends CustomSerializer[URI]( _ => ( { - case JString(s) => + case JString(s) => try new URI(s) catch { case _: URISyntaxException => @@ -25,21 +25,46 @@ object AdditionalTypeSerializers { } {{/json4s}} {{#circe}} +import java.io.File +import java.nio.file.Files + trait AdditionalTypeSerializers { - import io.circe._ - - implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => - try Right(new URI(string)) - catch { - case _: URISyntaxException => - Left("String could not be parsed as a URI reference, it violates RFC 2396.") - case _: NullPointerException => - Left("String is null.") - } - ) - - implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { - final def apply(a: URI): Json = Json.fromString(a.toString) + import io.circe._ + + implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => + try Right(new URI(string)) + catch { + case _: URISyntaxException => + Left("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + Left("String is null.") + } + ) + + implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { + final def apply(a: URI): Json = Json.fromString(a.toString) + } + + implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes => + try { + val tmpFile = File.createTempFile("download", ".tmp") + tmpFile.deleteOnExit() + Files.write(tmpFile.toPath, bytes) + Right(tmpFile) + } catch { + case e: Exception => Left(s"Failed to write binary content to file: ${e.getMessage}") + } + } + + implicit final lazy val FileEncoder: Encoder[File] = Encoder[Array[Byte]].contramap( + f => Files.readAllBytes(f.toPath) + ) + + implicit final lazy val AnyDecoder: Decoder[Any] = Decoder[Json].map(_.asInstanceOf[Any]) + + implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance { + case json: Json => json + case other => Json.fromString(other.toString) } } {{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache index 2dcb6109a860..ad452812c682 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache @@ -42,10 +42,9 @@ object JsonSupport extends SttpJson4sApi { {{/json4s}} {{#circe}} import io.circe.{Decoder, Encoder} -import io.circe.generic.AutoDerivation import sttp.client3.circe.SttpCirceApi -object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers { +object JsonSupport extends SttpCirceApi with DateSerializers with AdditionalTypeSerializers { {{#models}} {{#model}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache index ecece27a59a5..dbf085b6ea20 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache @@ -4,6 +4,11 @@ package {{package}} {{#imports}} import {{import}} {{/imports}} +{{#circe}} +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import {{invokerPackage}}.JsonSupport._ +{{/circe}} {{#models}} {{#model}} @@ -24,6 +29,30 @@ case class {{classname}}( {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} {{/vars}} ) +{{#circe}} +object {{classname}} { + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { t => + Json.fromFields{ + Seq( + {{#vars}} + {{#required}}Some("{{baseName}}" -> t.{{{name}}}.asJson){{/required}}{{^required}}t.{{{name}}}.map(v => "{{baseName}}" -> v.asJson){{/required}}{{^-last}},{{/-last}} + {{/vars}} + ).flatten + } + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + for { + {{#vars}} + {{{name}}} <- c.downField("{{baseName}}").as[{{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}}] + {{/vars}} + } yield {{classname}}( + {{#vars}} + {{{name}}} = {{{name}}}{{^-last}},{{/-last}} + {{/vars}} + ) + } +} +{{/circe}} {{/isEnum}} {{#isEnum}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java index c6f2150321bd..134c912b0bc7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java @@ -110,6 +110,73 @@ public void verifyApiKeyLocations() throws IOException { assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)"); } + @Test + public void circeSerdeWithMixedCaseFields() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/scala/mixed-case-fields.yaml", null, new ParseOptions()).getOpenAPI(); + + ScalaSttpClientCodegen codegen = new ScalaSttpClientCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put("jsonLibrary", "circe"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + generator.opts(input).generate(); + + Path modelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala"); + Path jsonSupportPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/JsonSupport.scala"); + + // Model should have camelCase Scala field names in the case class + assertFileContains(modelPath, "firstName"); + assertFileContains(modelPath, "phoneNumber"); + assertFileContains(modelPath, "lastName"); + assertFileContains(modelPath, "zipCode"); + assertFileContains(modelPath, "address"); + + // Encoder should use original baseName for JSON keys + assertFileContains(modelPath, "\"first-name\""); // kebab-case preserved + assertFileContains(modelPath, "\"phone_number\""); // snake_case preserved + assertFileContains(modelPath, "\"lastName\""); // camelCase preserved + assertFileContains(modelPath, "\"ZipCode\""); // PascalCase preserved + assertFileContains(modelPath, "\"address\""); // lowercase preserved + + // Decoder should use original baseName in downField + assertFileContains(modelPath, "c.downField(\"first-name\")"); + assertFileContains(modelPath, "c.downField(\"phone_number\")"); + assertFileContains(modelPath, "c.downField(\"ZipCode\")"); + + // Model should have explicit encoder/decoder companion object + assertFileContains(modelPath, "object MixedCaseModel"); + assertFileContains(modelPath, "implicit val encoderMixedCaseModel"); + assertFileContains(modelPath, "implicit val decoderMixedCaseModel"); + + // Model should import JsonSupport for enum implicits + assertFileContains(modelPath, "import org.openapitools.client.core.JsonSupport._"); + + // JsonSupport should NOT use AutoDerivation (we have explicit instances now) + assertFileNotContains(jsonSupportPath, "AutoDerivation"); + + // AdditionalTypeSerializers should have File and Any codecs for circe + Path additionalSerializersPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala"); + assertFileContains(additionalSerializersPath, "FileDecoder"); + assertFileContains(additionalSerializersPath, "FileEncoder"); + assertFileContains(additionalSerializersPath, "AnyDecoder"); + assertFileContains(additionalSerializersPath, "AnyEncoder"); + } + @Test public void headerSerialization() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml new file mode 100644 index 000000000000..0246e1d5c0b3 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.0 +info: + title: Mixed Case Test + version: 1.0.0 +paths: + /test: + get: + operationId: getTest + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/MixedCaseModel' +components: + schemas: + MixedCaseModel: + type: object + properties: + first-name: + type: string + phone_number: + type: string + lastName: + type: string + ZipCode: + type: string + address: + type: string diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala index 137dbc248fad..3f3f019726bd 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala @@ -2,20 +2,45 @@ package org.openapitools.client.core import java.net.{ URI, URISyntaxException } +import java.io.File +import java.nio.file.Files + trait AdditionalTypeSerializers { - import io.circe._ - - implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => - try Right(new URI(string)) - catch { - case _: URISyntaxException => - Left("String could not be parsed as a URI reference, it violates RFC 2396.") - case _: NullPointerException => - Left("String is null.") - } - ) - - implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { - final def apply(a: URI): Json = Json.fromString(a.toString) + import io.circe._ + + implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => + try Right(new URI(string)) + catch { + case _: URISyntaxException => + Left("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + Left("String is null.") + } + ) + + implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { + final def apply(a: URI): Json = Json.fromString(a.toString) + } + + implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes => + try { + val tmpFile = File.createTempFile("download", ".tmp") + tmpFile.deleteOnExit() + Files.write(tmpFile.toPath, bytes) + Right(tmpFile) + } catch { + case e: Exception => Left(s"Failed to write binary content to file: ${e.getMessage}") + } + } + + implicit final lazy val FileEncoder: Encoder[File] = Encoder[Array[Byte]].contramap( + f => Files.readAllBytes(f.toPath) + ) + + implicit final lazy val AnyDecoder: Decoder[Any] = Decoder[Json].map(_.asInstanceOf[Any]) + + implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance { + case json: Json => json + case other => Json.fromString(other.toString) } } diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala index 4d3dfa527521..3f6dfee76235 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala @@ -13,10 +13,9 @@ package org.openapitools.client.core import org.openapitools.client.model._ import io.circe.{Decoder, Encoder} -import io.circe.generic.AutoDerivation import sttp.client3.circe.SttpCirceApi -object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers { +object JsonSupport extends SttpCirceApi with DateSerializers with AdditionalTypeSerializers { implicit val EnumTestSearchDecoder: Decoder[EnumTestEnums.Search] = Decoder.decodeEnumeration(EnumTestEnums.Search) implicit val EnumTestSearchEncoder: Encoder[EnumTestEnums.Search] = Encoder.encodeEnumeration(EnumTestEnums.Search) diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala index b0abb512265e..514f416b07a1 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * An uploaded response @@ -21,4 +24,26 @@ case class ApiResponse( `type`: Option[String] = None, message: Option[String] = None ) +object ApiResponse { + implicit val encoderApiResponse: Encoder[ApiResponse] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.code.map(v => "code" -> v.asJson), + t.`type`.map(v => "type" -> v.asJson), + t.message.map(v => "message" -> v.asJson) + ).flatten + } + } + implicit val decoderApiResponse: Decoder[ApiResponse] = Decoder.instance { c => + for { + code <- c.downField("code").as[Option[Int]] + `type` <- c.downField("type").as[Option[String]] + message <- c.downField("message").as[Option[String]] + } yield ApiResponse( + code = code, + `type` = `type`, + message = message + ) + } +} diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala index 169acc49cefd..c03cc7fc6a1f 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * Pet category @@ -20,4 +23,23 @@ case class Category( id: Option[Long] = None, name: Option[String] = None ) +object Category { + implicit val encoderCategory: Encoder[Category] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.id.map(v => "id" -> v.asJson), + t.name.map(v => "name" -> v.asJson) + ).flatten + } + } + implicit val decoderCategory: Decoder[Category] = Decoder.instance { c => + for { + id <- c.downField("id").as[Option[Long]] + name <- c.downField("name").as[Option[String]] + } yield Category( + id = id, + name = name + ) + } +} diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala index 96fad75f85df..27de86d3402d 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala @@ -11,12 +11,37 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ case class EnumTest( emails: Option[Seq[String]] = None, search: Option[EnumTestEnums.Search] = None, sortBy: Option[Seq[EnumTestEnums.SortBy]] = None ) +object EnumTest { + implicit val encoderEnumTest: Encoder[EnumTest] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.emails.map(v => "emails" -> v.asJson), + t.search.map(v => "search" -> v.asJson), + t.sortBy.map(v => "sort_by" -> v.asJson) + ).flatten + } + } + implicit val decoderEnumTest: Decoder[EnumTest] = Decoder.instance { c => + for { + emails <- c.downField("emails").as[Option[Seq[String]]] + search <- c.downField("search").as[Option[EnumTestEnums.Search]] + sortBy <- c.downField("sort_by").as[Option[Seq[EnumTestEnums.SortBy]]] + } yield EnumTest( + emails = emails, + search = search, + sortBy = sortBy + ) + } +} object EnumTestEnums { diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala index 9e805e256b21..a3bf7ea39dcf 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala @@ -12,6 +12,9 @@ package org.openapitools.client.model import java.time.OffsetDateTime +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * Pet Order @@ -26,6 +29,37 @@ case class Order( status: Option[OrderEnums.Status] = None, complete: Option[Boolean] = None ) +object Order { + implicit val encoderOrder: Encoder[Order] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.id.map(v => "id" -> v.asJson), + t.petId.map(v => "petId" -> v.asJson), + t.quantity.map(v => "quantity" -> v.asJson), + t.shipDate.map(v => "shipDate" -> v.asJson), + t.status.map(v => "status" -> v.asJson), + t.complete.map(v => "complete" -> v.asJson) + ).flatten + } + } + implicit val decoderOrder: Decoder[Order] = Decoder.instance { c => + for { + id <- c.downField("id").as[Option[Long]] + petId <- c.downField("petId").as[Option[Long]] + quantity <- c.downField("quantity").as[Option[Int]] + shipDate <- c.downField("shipDate").as[Option[OffsetDateTime]] + status <- c.downField("status").as[Option[OrderEnums.Status]] + complete <- c.downField("complete").as[Option[Boolean]] + } yield Order( + id = id, + petId = petId, + quantity = quantity, + shipDate = shipDate, + status = status, + complete = complete + ) + } +} object OrderEnums { diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala index 3cbab6051284..e9f8b4b2eb61 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * a Pet @@ -25,6 +28,37 @@ case class Pet( /* pet status in the store */ status: Option[PetEnums.Status] = None ) +object Pet { + implicit val encoderPet: Encoder[Pet] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.id.map(v => "id" -> v.asJson), + t.category.map(v => "category" -> v.asJson), + Some("name" -> t.name.asJson), + Some("photoUrls" -> t.photoUrls.asJson), + t.tags.map(v => "tags" -> v.asJson), + t.status.map(v => "status" -> v.asJson) + ).flatten + } + } + implicit val decoderPet: Decoder[Pet] = Decoder.instance { c => + for { + id <- c.downField("id").as[Option[Long]] + category <- c.downField("category").as[Option[Category]] + name <- c.downField("name").as[String] + photoUrls <- c.downField("photoUrls").as[Seq[String]] + tags <- c.downField("tags").as[Option[Seq[Tag]]] + status <- c.downField("status").as[Option[PetEnums.Status]] + } yield Pet( + id = id, + category = category, + name = name, + photoUrls = photoUrls, + tags = tags, + status = status + ) + } +} object PetEnums { diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala index 3782f4c12be3..cd870e77f4e9 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ case class PropertyNameMapping( `httpDebugOperation`: Option[String] = None, @@ -18,4 +21,29 @@ case class PropertyNameMapping( `type`: Option[String] = None, `typeWithUnderscore`: Option[String] = None ) +object PropertyNameMapping { + implicit val encoderPropertyNameMapping: Encoder[PropertyNameMapping] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.`httpDebugOperation`.map(v => "http_debug_operation" -> v.asJson), + t.`underscoreType`.map(v => "_type" -> v.asJson), + t.`type`.map(v => "type" -> v.asJson), + t.`typeWithUnderscore`.map(v => "type_" -> v.asJson) + ).flatten + } + } + implicit val decoderPropertyNameMapping: Decoder[PropertyNameMapping] = Decoder.instance { c => + for { + `httpDebugOperation` <- c.downField("http_debug_operation").as[Option[String]] + `underscoreType` <- c.downField("_type").as[Option[String]] + `type` <- c.downField("type").as[Option[String]] + `typeWithUnderscore` <- c.downField("type_").as[Option[String]] + } yield PropertyNameMapping( + `httpDebugOperation` = `httpDebugOperation`, + `underscoreType` = `underscoreType`, + `type` = `type`, + `typeWithUnderscore` = `typeWithUnderscore` + ) + } +} diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala index c2020246658a..d1b9289b90cf 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * Pet Tag @@ -20,4 +23,23 @@ case class Tag( id: Option[Long] = None, name: Option[String] = None ) +object Tag { + implicit val encoderTag: Encoder[Tag] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.id.map(v => "id" -> v.asJson), + t.name.map(v => "name" -> v.asJson) + ).flatten + } + } + implicit val decoderTag: Decoder[Tag] = Decoder.instance { c => + for { + id <- c.downField("id").as[Option[Long]] + name <- c.downField("name").as[Option[String]] + } yield Tag( + id = id, + name = name + ) + } +} diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala index 6977180bccee..339936997b69 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala @@ -11,6 +11,9 @@ */ package org.openapitools.client.model +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ /** * a User @@ -27,4 +30,41 @@ case class User( /* User Status */ userStatus: Option[Int] = None ) +object User { + implicit val encoderUser: Encoder[User] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.id.map(v => "id" -> v.asJson), + t.username.map(v => "username" -> v.asJson), + t.firstName.map(v => "firstName" -> v.asJson), + t.lastName.map(v => "lastName" -> v.asJson), + t.email.map(v => "email" -> v.asJson), + t.password.map(v => "password" -> v.asJson), + t.phone.map(v => "phone" -> v.asJson), + t.userStatus.map(v => "userStatus" -> v.asJson) + ).flatten + } + } + implicit val decoderUser: Decoder[User] = Decoder.instance { c => + for { + id <- c.downField("id").as[Option[Long]] + username <- c.downField("username").as[Option[String]] + firstName <- c.downField("firstName").as[Option[String]] + lastName <- c.downField("lastName").as[Option[String]] + email <- c.downField("email").as[Option[String]] + password <- c.downField("password").as[Option[String]] + phone <- c.downField("phone").as[Option[String]] + userStatus <- c.downField("userStatus").as[Option[Int]] + } yield User( + id = id, + username = username, + firstName = firstName, + lastName = lastName, + email = email, + password = password, + phone = phone, + userStatus = userStatus + ) + } +} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala index 2ac2a4b19275..2e254a1b67ae 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala @@ -6,7 +6,7 @@ object AdditionalTypeSerializers { import org.json4s.{Serializer, CustomSerializer, JNull, MappingException} import org.json4s.JsonAST.JString case object URISerializer extends CustomSerializer[URI]( _ => ( { - case JString(s) => + case JString(s) => try new URI(s) catch { case _: URISyntaxException => diff --git a/samples/yaml/user.yml b/samples/yaml/user.yml index 914b8b7778b0..e69de29bb2d1 100644 --- a/samples/yaml/user.yml +++ b/samples/yaml/user.yml @@ -1,178 +0,0 @@ -apiVersion: 1.0.0 -swaggerVersion: "1.2" -basePath: "http://localhost:8002/api" -resourcePath: /user -produces: - - application/json - - application/xml -apis: - - path: /user - operations: - - method: POST - summary: Create user - notes: This can only be done by the logged in user. - type: void - nickname: createUser - parameters: - - name: body - description: Created user object - required: true - allowMultiple: false - type: User - paramType: body - - path: /user/createWithArray - operations: - - method: POST - summary: Creates list of users with given input array - notes: "" - type: void - nickname: createUsersWithArrayInput - parameters: - - name: body - description: List of user object - required: true - allowMultiple: false - type: array - items: - $ref: User - paramType: body - - path: /user/createWithList - operations: - - method: POST - summary: Creates list of users with given list input - notes: "" - type: void - nickname: createUsersWithListInput - parameters: - - name: body - description: List of user object - required: true - allowMultiple: false - type: array - items: - $ref: User - paramType: body - - path: "/user/{username}" - operations: - - method: PUT - summary: Updated user - notes: This can only be done by the logged in user. - type: void - nickname: updateUser - parameters: - - name: username - description: name that need to be deleted - required: true - allowMultiple: false - type: string - paramType: path - - name: body - description: Updated user object - required: true - allowMultiple: false - type: User - paramType: body - responseMessages: - - code: 400 - message: Invalid username supplied - - code: 404 - message: User not found - - method: DELETE - summary: Delete user - notes: This can only be done by the logged in user. - type: void - nickname: deleteUser - parameters: - - name: username - description: The name that needs to be deleted - required: true - allowMultiple: false - type: string - paramType: path - responseMessages: - - code: 400 - message: Invalid username supplied - - code: 404 - message: User not found - - method: GET - summary: Get user by user name - notes: "" - type: User - nickname: getUserByName - produces: - - application/json - - application/xml - parameters: - - name: username - description: The name that needs to be fetched. Use user1 for testing. - required: true - allowMultiple: false - type: string - paramType: path - responseMessages: - - code: 400 - message: Invalid username supplied - - code: 404 - message: User not found - - path: /user/login - operations: - - method: GET - summary: Logs user into the system - notes: "" - type: string - nickname: loginUser - produces: - - text/plain - parameters: - - name: username - description: The user name for login - required: true - allowMultiple: false - type: string - paramType: query - - name: password - description: The password for login in clear text - required: true - allowMultiple: false - type: string - paramType: query - responseMessages: - - code: 400 - message: Invalid username and password combination - - path: /user/logout - operations: - - method: GET - summary: Logs out current logged in user session - notes: "" - type: void - nickname: logoutUser - produces: - - text/plain - parameters: [] -models: - User: - id: User - properties: - id: - type: integer - format: int64 - username: - type: string - password: - type: string - email: - type: string - firstName: - type: string - lastName: - type: string - phone: - type: string - userStatus: - type: integer - format: int32 - description: User Status - enum: - - 1-registered - - 2-active - - 3-closed \ No newline at end of file From 54bb2e85069bb3bb821785c982c5a8d3c5413a3d Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Tue, 7 Apr 2026 17:05:31 -0700 Subject: [PATCH 2/8] update tests --- .../codegen/scala/SttpCodegenTest.java | 54 +++++++++---------- .../3_0/scala/mixed-case-fields.yaml | 8 +++ 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java index 134c912b0bc7..0cfc0568aee7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java @@ -111,7 +111,7 @@ public void verifyApiKeyLocations() throws IOException { } @Test - public void circeSerdeWithMixedCaseFields() throws IOException { + public void verifyCirceSerdeWithMixedCaseFields() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); output.deleteOnExit(); String outputPath = output.getAbsolutePath().replace('\\', '/'); @@ -136,40 +136,36 @@ public void circeSerdeWithMixedCaseFields() throws IOException { generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); generator.opts(input).generate(); - Path modelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala"); - Path jsonSupportPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/JsonSupport.scala"); + Path mixedCaseModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala"); - // Model should have camelCase Scala field names in the case class - assertFileContains(modelPath, "firstName"); - assertFileContains(modelPath, "phoneNumber"); - assertFileContains(modelPath, "lastName"); - assertFileContains(modelPath, "zipCode"); - assertFileContains(modelPath, "address"); + assertFileContains(mixedCaseModelPath, "firstName"); + assertFileContains(mixedCaseModelPath, "phoneNumber"); + assertFileContains(mixedCaseModelPath, "lastName"); + assertFileContains(mixedCaseModelPath, "zipCode"); + assertFileContains(mixedCaseModelPath, "address"); - // Encoder should use original baseName for JSON keys - assertFileContains(modelPath, "\"first-name\""); // kebab-case preserved - assertFileContains(modelPath, "\"phone_number\""); // snake_case preserved - assertFileContains(modelPath, "\"lastName\""); // camelCase preserved - assertFileContains(modelPath, "\"ZipCode\""); // PascalCase preserved - assertFileContains(modelPath, "\"address\""); // lowercase preserved + assertFileContains(mixedCaseModelPath, "\"first-name\""); + assertFileContains(mixedCaseModelPath, "\"phone_number\""); + assertFileContains(mixedCaseModelPath, "\"lastName\""); + assertFileContains(mixedCaseModelPath, "\"ZipCode\""); + assertFileContains(mixedCaseModelPath, "\"address\""); - // Decoder should use original baseName in downField - assertFileContains(modelPath, "c.downField(\"first-name\")"); - assertFileContains(modelPath, "c.downField(\"phone_number\")"); - assertFileContains(modelPath, "c.downField(\"ZipCode\")"); + assertFileContains(mixedCaseModelPath, "c.downField(\"first-name\")"); + assertFileContains(mixedCaseModelPath, "c.downField(\"phone_number\")"); + assertFileContains(mixedCaseModelPath, "c.downField(\"ZipCode\")"); - // Model should have explicit encoder/decoder companion object - assertFileContains(modelPath, "object MixedCaseModel"); - assertFileContains(modelPath, "implicit val encoderMixedCaseModel"); - assertFileContains(modelPath, "implicit val decoderMixedCaseModel"); + assertFileContains(mixedCaseModelPath, "object MixedCaseModel"); + assertFileContains(mixedCaseModelPath, "implicit val encoderMixedCaseModel"); + assertFileContains(mixedCaseModelPath, "implicit val decoderMixedCaseModel"); - // Model should import JsonSupport for enum implicits - assertFileContains(modelPath, "import org.openapitools.client.core.JsonSupport._"); + Path binaryModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/BinaryPayload.scala"); + assertFileContains(binaryModelPath, "data: Option[java.io.File]"); + assertFileContains(binaryModelPath, "metadata: Option[Any]"); + assertFileContains(binaryModelPath, "c.downField(\"data\")"); + assertFileContains(binaryModelPath, "c.downField(\"metadata\")"); + assertFileContains(binaryModelPath, "implicit val encoderBinaryPayload"); + assertFileContains(binaryModelPath, "implicit val decoderBinaryPayload"); - // JsonSupport should NOT use AutoDerivation (we have explicit instances now) - assertFileNotContains(jsonSupportPath, "AutoDerivation"); - - // AdditionalTypeSerializers should have File and Any codecs for circe Path additionalSerializersPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala"); assertFileContains(additionalSerializersPath, "FileDecoder"); assertFileContains(additionalSerializersPath, "FileEncoder"); diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml index 0246e1d5c0b3..6b78aa8fc08a 100644 --- a/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml @@ -28,3 +28,11 @@ components: type: string address: type: string + BinaryPayload: + type: object + properties: + data: + type: string + format: binary + metadata: + type: object From 0d11e4e0f7a73e5f299890a9bbcd35dcda065e74 Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Tue, 7 Apr 2026 17:26:52 -0700 Subject: [PATCH 3/8] Fix bug --- .../codegen/languages/ScalaSttpClientCodegen.java | 13 +++++++------ .../openapitools/codegen/scala/SttpCodegenTest.java | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java index bc3a3f191bb7..c3266f72bb86 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java @@ -108,10 +108,6 @@ public ScalaSttpClientCodegen() { apiTemplateFiles.put("api.mustache", ".scala"); embeddedTemplateDir = templateDir = "scala-sttp"; - String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); - - String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue"; - additionalProperties.put(CodegenConstants.GROUP_ID, groupId); additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); @@ -141,13 +137,13 @@ public ScalaSttpClientCodegen() { typeMapping.put("short", "Short"); typeMapping.put("char", "Char"); typeMapping.put("double", "Double"); - typeMapping.put("object", jsonValueClass); + typeMapping.put("object", "Any"); typeMapping.put("file", "File"); typeMapping.put("binary", "File"); typeMapping.put("number", "Double"); typeMapping.put("decimal", "BigDecimal"); typeMapping.put("ByteArray", "Array[Byte]"); - typeMapping.put("AnyType", jsonValueClass); + typeMapping.put("AnyType", "Any"); instantiationTypes.put("array", "ListBuffer"); instantiationTypes.put("map", "Map"); @@ -166,6 +162,11 @@ public void processOpts() { apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties); modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties); + String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); + String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue"; + typeMapping.put("object", jsonValueClass); + typeMapping.put("AnyType", jsonValueClass); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt")); final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java index 0cfc0568aee7..e5f24e8d722f 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java @@ -159,8 +159,8 @@ public void verifyCirceSerdeWithMixedCaseFields() throws IOException { assertFileContains(mixedCaseModelPath, "implicit val decoderMixedCaseModel"); Path binaryModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/BinaryPayload.scala"); - assertFileContains(binaryModelPath, "data: Option[java.io.File]"); - assertFileContains(binaryModelPath, "metadata: Option[Any]"); + assertFileContains(binaryModelPath, "data: Option[File]"); + assertFileContains(binaryModelPath, "metadata: Option[io.circe.Json]"); assertFileContains(binaryModelPath, "c.downField(\"data\")"); assertFileContains(binaryModelPath, "c.downField(\"metadata\")"); assertFileContains(binaryModelPath, "implicit val encoderBinaryPayload"); From b5cef9090e071e693d52c8272eaddb1a545de58b Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Tue, 7 Apr 2026 17:31:12 -0700 Subject: [PATCH 4/8] Revert accidental delete --- samples/yaml/user.yml | 178 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/samples/yaml/user.yml b/samples/yaml/user.yml index e69de29bb2d1..914b8b7778b0 100644 --- a/samples/yaml/user.yml +++ b/samples/yaml/user.yml @@ -0,0 +1,178 @@ +apiVersion: 1.0.0 +swaggerVersion: "1.2" +basePath: "http://localhost:8002/api" +resourcePath: /user +produces: + - application/json + - application/xml +apis: + - path: /user + operations: + - method: POST + summary: Create user + notes: This can only be done by the logged in user. + type: void + nickname: createUser + parameters: + - name: body + description: Created user object + required: true + allowMultiple: false + type: User + paramType: body + - path: /user/createWithArray + operations: + - method: POST + summary: Creates list of users with given input array + notes: "" + type: void + nickname: createUsersWithArrayInput + parameters: + - name: body + description: List of user object + required: true + allowMultiple: false + type: array + items: + $ref: User + paramType: body + - path: /user/createWithList + operations: + - method: POST + summary: Creates list of users with given list input + notes: "" + type: void + nickname: createUsersWithListInput + parameters: + - name: body + description: List of user object + required: true + allowMultiple: false + type: array + items: + $ref: User + paramType: body + - path: "/user/{username}" + operations: + - method: PUT + summary: Updated user + notes: This can only be done by the logged in user. + type: void + nickname: updateUser + parameters: + - name: username + description: name that need to be deleted + required: true + allowMultiple: false + type: string + paramType: path + - name: body + description: Updated user object + required: true + allowMultiple: false + type: User + paramType: body + responseMessages: + - code: 400 + message: Invalid username supplied + - code: 404 + message: User not found + - method: DELETE + summary: Delete user + notes: This can only be done by the logged in user. + type: void + nickname: deleteUser + parameters: + - name: username + description: The name that needs to be deleted + required: true + allowMultiple: false + type: string + paramType: path + responseMessages: + - code: 400 + message: Invalid username supplied + - code: 404 + message: User not found + - method: GET + summary: Get user by user name + notes: "" + type: User + nickname: getUserByName + produces: + - application/json + - application/xml + parameters: + - name: username + description: The name that needs to be fetched. Use user1 for testing. + required: true + allowMultiple: false + type: string + paramType: path + responseMessages: + - code: 400 + message: Invalid username supplied + - code: 404 + message: User not found + - path: /user/login + operations: + - method: GET + summary: Logs user into the system + notes: "" + type: string + nickname: loginUser + produces: + - text/plain + parameters: + - name: username + description: The user name for login + required: true + allowMultiple: false + type: string + paramType: query + - name: password + description: The password for login in clear text + required: true + allowMultiple: false + type: string + paramType: query + responseMessages: + - code: 400 + message: Invalid username and password combination + - path: /user/logout + operations: + - method: GET + summary: Logs out current logged in user session + notes: "" + type: void + nickname: logoutUser + produces: + - text/plain + parameters: [] +models: + User: + id: User + properties: + id: + type: integer + format: int64 + username: + type: string + password: + type: string + email: + type: string + firstName: + type: string + lastName: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + enum: + - 1-registered + - 2-active + - 3-closed \ No newline at end of file From 596a75037f045da10fd71ed881011496274cbb60 Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Tue, 7 Apr 2026 17:42:44 -0700 Subject: [PATCH 5/8] [AI review feedback] Fix bugs --- .../scala-sttp/additionalTypeSerializers.mustache | 7 ++++++- .../src/main/resources/scala-sttp/model.mustache | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache index a8c11eb2a5c7..bbac20f10325 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/additionalTypeSerializers.mustache @@ -48,7 +48,6 @@ trait AdditionalTypeSerializers { implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes => try { val tmpFile = File.createTempFile("download", ".tmp") - tmpFile.deleteOnExit() Files.write(tmpFile.toPath, bytes) Right(tmpFile) } catch { @@ -64,6 +63,12 @@ trait AdditionalTypeSerializers { implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance { case json: Json => json + case b: Boolean => Json.fromBoolean(b) + case n: Int => Json.fromInt(n) + case n: Long => Json.fromLong(n) + case n: Double => Json.fromDoubleOrNull(n) + case n: BigDecimal => Json.fromBigDecimal(n) + case s: String => Json.fromString(s) case other => Json.fromString(other.toString) } } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache index dbf085b6ea20..e584c94da3b1 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache @@ -31,6 +31,7 @@ case class {{classname}}( ) {{#circe}} object {{classname}} { +{{#hasVars}} implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { t => Json.fromFields{ Seq( @@ -51,6 +52,15 @@ object {{classname}} { {{/vars}} ) } +{{/hasVars}} +{{^hasVars}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { _ => + Json.fromFields(Seq.empty) + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { _ => + Right({{classname}}()) + } +{{/hasVars}} } {{/circe}} {{/isEnum}} From 8120c8535e83ae628cd88e956b1796d6fee2a9ed Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Wed, 8 Apr 2026 11:15:39 -0700 Subject: [PATCH 6/8] Regen Samples --- .../client/core/AdditionalTypeSerializers.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala index 3f3f019726bd..962c4a002f32 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala @@ -25,7 +25,6 @@ trait AdditionalTypeSerializers { implicit final lazy val FileDecoder: Decoder[File] = Decoder[Array[Byte]].emap { bytes => try { val tmpFile = File.createTempFile("download", ".tmp") - tmpFile.deleteOnExit() Files.write(tmpFile.toPath, bytes) Right(tmpFile) } catch { @@ -41,6 +40,12 @@ trait AdditionalTypeSerializers { implicit final lazy val AnyEncoder: Encoder[Any] = Encoder.instance { case json: Json => json + case b: Boolean => Json.fromBoolean(b) + case n: Int => Json.fromInt(n) + case n: Long => Json.fromLong(n) + case n: Double => Json.fromDoubleOrNull(n) + case n: BigDecimal => Json.fromBigDecimal(n) + case s: String => Json.fromString(s) case other => Json.fromString(other.toString) } } From 0e38b8ea43ec019dba2016a59201eb52327f993d Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Wed, 8 Apr 2026 11:20:49 -0700 Subject: [PATCH 7/8] Regen Docs --- .../README.md | 2 +- .../org/openapitools/server/models/Cat.kt | 14 ++++++------- .../org/openapitools/server/models/Dog.kt | 21 +++++++------------ .../org/openapitools/server/models/Pet.kt | 13 +++++------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md index d3a5056004b5..6a1eb18ec292 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md @@ -1,4 +1,4 @@ -# org.openapitools.server - Kotlin Server library for Polymorphism example with allOf and discriminator +# org.openapitools.server - Kotlin Server library for Basic polymorphism example with discriminator No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt index e9e835517bae..99ab4e0f3f50 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -1,5 +1,5 @@ /** - * Polymorphism example with allOf and discriminator + * Basic polymorphism example with discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,11 +11,11 @@ */ package org.openapitools.server.models -import org.openapitools.server.models.Pet /** - * A representation of a cat + * A pet cat * @param huntingSkill The measured skill for hunting + * @param petType */ data class Cat( /* The measured skill for hunting */ @@ -23,12 +23,10 @@ data class Cat( @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill") val huntingSkill: Cat.HuntingSkill, - @field:com.fasterxml.jackson.annotation.JsonProperty("name") - override val name: kotlin.String, - @field:com.fasterxml.jackson.annotation.JsonProperty("petType") - override val petType: kotlin.String -) : Pet(name = name, petType = petType) + override val petType: kotlin.String = "cat", + +) : Pet(petType = petType) { /** * The measured skill for hunting diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt index 4066cb7a51f8..9f3cbe860d93 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -1,5 +1,5 @@ /** - * Polymorphism example with allOf and discriminator + * Basic polymorphism example with discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,24 +11,19 @@ */ package org.openapitools.server.models -import org.openapitools.server.models.Pet /** - * A representation of a dog + * A pet dog + * @param petType * @param packSize the size of the pack the dog is from */ data class Dog( + + @field:com.fasterxml.jackson.annotation.JsonProperty("petType") + override val petType: kotlin.String = "dog", /* the size of the pack the dog is from */ @field:com.fasterxml.jackson.annotation.JsonProperty("packSize") - val packSize: kotlin.Int = 0, - - @field:com.fasterxml.jackson.annotation.JsonProperty("name") - override val name: kotlin.String, - - @field:com.fasterxml.jackson.annotation.JsonProperty("petType") - override val petType: kotlin.String -) : Pet(name = name, petType = petType) -{ -} + val packSize: kotlin.Int = 0 +) : Pet(petType = petType) diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt index 5812ac1944eb..1311dc6d56ff 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -1,5 +1,5 @@ /** - * Polymorphism example with allOf and discriminator + * Basic polymorphism example with discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,23 +11,20 @@ */ package org.openapitools.server.models +import org.openapitools.server.models.Cat +import org.openapitools.server.models.Dog /** * - * @param name * @param petType */ @com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true) @com.fasterxml.jackson.annotation.JsonSubTypes( - com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "Cat"), - com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "Dog") + com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"), + com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog") ) sealed class Pet( - @field:com.fasterxml.jackson.annotation.JsonProperty("name") - open val name: kotlin.String -, - @field:com.fasterxml.jackson.annotation.JsonProperty("petType") open val petType: kotlin.String From 09c7821aeb55b2164c52b18e6f3be6208d07a26c Mon Sep 17 00:00:00 2001 From: Nikhil Sulegaon Date: Wed, 8 Apr 2026 12:06:52 -0700 Subject: [PATCH 8/8] Fix kotlin-server samples --- .../README.md | 2 +- .../org/openapitools/server/models/Cat.kt | 14 +++++++------ .../org/openapitools/server/models/Dog.kt | 21 ++++++++++++------- .../org/openapitools/server/models/Pet.kt | 13 +++++++----- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md index 6a1eb18ec292..d3a5056004b5 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/README.md @@ -1,4 +1,4 @@ -# org.openapitools.server - Kotlin Server library for Basic polymorphism example with discriminator +# org.openapitools.server - Kotlin Server library for Polymorphism example with allOf and discriminator No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt index 99ab4e0f3f50..e9e835517bae 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -1,5 +1,5 @@ /** - * Basic polymorphism example with discriminator + * Polymorphism example with allOf and discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,11 +11,11 @@ */ package org.openapitools.server.models +import org.openapitools.server.models.Pet /** - * A pet cat + * A representation of a cat * @param huntingSkill The measured skill for hunting - * @param petType */ data class Cat( /* The measured skill for hunting */ @@ -23,10 +23,12 @@ data class Cat( @field:com.fasterxml.jackson.annotation.JsonProperty("huntingSkill") val huntingSkill: Cat.HuntingSkill, + @field:com.fasterxml.jackson.annotation.JsonProperty("name") + override val name: kotlin.String, + @field:com.fasterxml.jackson.annotation.JsonProperty("petType") - override val petType: kotlin.String = "cat", - -) : Pet(petType = petType) + override val petType: kotlin.String +) : Pet(name = name, petType = petType) { /** * The measured skill for hunting diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt index 9f3cbe860d93..4066cb7a51f8 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -1,5 +1,5 @@ /** - * Basic polymorphism example with discriminator + * Polymorphism example with allOf and discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,19 +11,24 @@ */ package org.openapitools.server.models +import org.openapitools.server.models.Pet /** - * A pet dog - * @param petType + * A representation of a dog * @param packSize the size of the pack the dog is from */ data class Dog( - - @field:com.fasterxml.jackson.annotation.JsonProperty("petType") - override val petType: kotlin.String = "dog", /* the size of the pack the dog is from */ @field:com.fasterxml.jackson.annotation.JsonProperty("packSize") - val packSize: kotlin.Int = 0 -) : Pet(petType = petType) + val packSize: kotlin.Int = 0, + + @field:com.fasterxml.jackson.annotation.JsonProperty("name") + override val name: kotlin.String, + + @field:com.fasterxml.jackson.annotation.JsonProperty("petType") + override val petType: kotlin.String +) : Pet(name = name, petType = petType) +{ +} diff --git a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt index 1311dc6d56ff..5812ac1944eb 100644 --- a/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt +++ b/samples/server/others/kotlin-server/polymorphism-allof-and-discriminator/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -1,5 +1,5 @@ /** - * Basic polymorphism example with discriminator + * Polymorphism example with allOf and discriminator * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) * * The version of the OpenAPI document: 1.0 @@ -11,20 +11,23 @@ */ package org.openapitools.server.models -import org.openapitools.server.models.Cat -import org.openapitools.server.models.Dog /** * + * @param name * @param petType */ @com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "petType", visible = true) @com.fasterxml.jackson.annotation.JsonSubTypes( - com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "cat"), - com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "dog") + com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "Cat"), + com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "Dog") ) sealed class Pet( + @field:com.fasterxml.jackson.annotation.JsonProperty("name") + open val name: kotlin.String +, + @field:com.fasterxml.jackson.annotation.JsonProperty("petType") open val petType: kotlin.String