Skip to content

Commit 21efd98

Browse files
committed
fix: use propertyBaseName for discriminator in sttp4 serialization
When the discriminator property name is a Scala keyword (e.g., `type`), the codegen escapes it with backticks for use as a Scala identifier. However, the serialization templates (circe and json4s) were using `discriminator.propertyName` which contains the escaped form, producing incorrect JSON field names like `\`type\`` instead of `type`. Switch all 10 usages in model.mustache to `discriminator.propertyBaseName` which holds the original, unescaped wire name from the OpenAPI spec. Add test case with Shape/Circle/Square using `type` as discriminator.
1 parent 79e738b commit 21efd98

3 files changed

Lines changed: 54 additions & 10 deletions

File tree

modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ object {{classname}} {
9494
implicit object {{classname}}Serializer extends Serializer[{{classname}}] {
9595
def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{classname}}] = {
9696
case (TypeInfo(clazz, _), json) if classOf[{{classname}}].isAssignableFrom(clazz) =>
97-
(json \ "{{discriminator.propertyName}}") match {
97+
(json \ "{{discriminator.propertyBaseName}}") match {
9898
{{#oneOf}}
9999
case JString("{{.}}") => Extraction.extract[{{.}}](json)
100100
{{/oneOf}}
@@ -104,7 +104,7 @@ object {{classname}} {
104104

105105
def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
106106
{{#oneOf}}
107-
case x: {{.}} => Extraction.decompose(x).merge(JObject("{{discriminator.propertyName}}" -> JString("{{.}}")))
107+
case x: {{.}} => Extraction.decompose(x).merge(JObject("{{discriminator.propertyBaseName}}" -> JString("{{.}}")))
108108
{{/oneOf}}
109109
}
110110
}
@@ -150,7 +150,7 @@ object {{classname}} {
150150
implicit object {{classname}}Serializer extends Serializer[{{classname}}] {
151151
def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{classname}}] = {
152152
case (TypeInfo(clazz, _), json) if classOf[{{classname}}].isAssignableFrom(clazz) =>
153-
(json \ "{{discriminator.propertyName}}") match {
153+
(json \ "{{discriminator.propertyBaseName}}") match {
154154
{{#vendorExtensions.x-oneOfMembers}}
155155
case JString("{{vendorExtensions.x-discriminator-value}}") => Extraction.extract[{{classname}}](json)
156156
{{/vendorExtensions.x-oneOfMembers}}
@@ -163,10 +163,10 @@ object {{classname}} {
163163

164164
def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
165165
{{#vendorExtensions.x-oneOfMembers}}
166-
case x: {{classname}} => Extraction.decompose(x).merge(JObject("{{discriminator.propertyName}}" -> JString("{{vendorExtensions.x-discriminator-value}}")))
166+
case x: {{classname}} => Extraction.decompose(x).merge(JObject("{{discriminator.propertyBaseName}}" -> JString("{{vendorExtensions.x-discriminator-value}}")))
167167
{{/vendorExtensions.x-oneOfMembers}}
168168
{{#vendorExtensions.x-wrappedOneOfMembers}}
169-
case {{wrapperClassname}}(v) => Extraction.decompose(v).merge(JObject("{{discriminator.propertyName}}" -> JString("{{discriminatorValue}}")))
169+
case {{wrapperClassname}}(v) => Extraction.decompose(v).merge(JObject("{{discriminator.propertyBaseName}}" -> JString("{{discriminatorValue}}")))
170170
{{/vendorExtensions.x-wrappedOneOfMembers}}
171171
}
172172
}
@@ -189,7 +189,7 @@ object {{classname}} {
189189
import io.circe.generic.extras._
190190
import io.circe.generic.extras.semiauto._
191191

192-
private implicit val config: Configuration = Configuration.default.withDiscriminator("{{discriminator.propertyName}}")
192+
private implicit val config: Configuration = Configuration.default.withDiscriminator("{{discriminator.propertyBaseName}}")
193193
.copy(
194194
transformConstructorNames = {
195195
{{#vendorExtensions.x-oneOfMembers}}
@@ -229,22 +229,22 @@ object {{classname}} {
229229
// oneOf with discriminator (with wrapper composition)
230230
implicit val encoder: Encoder[{{classname}}] = Encoder.instance {
231231
{{#vendorExtensions.x-oneOfMembers}}
232-
case x: {{classname}} => {{classname}}.encoder(x).deepMerge(io.circe.Json.obj("{{discriminator.propertyName}}" -> "{{vendorExtensions.x-discriminator-value}}".asJson))
232+
case x: {{classname}} => {{classname}}.encoder(x).deepMerge(io.circe.Json.obj("{{discriminator.propertyBaseName}}" -> "{{vendorExtensions.x-discriminator-value}}".asJson))
233233
{{/vendorExtensions.x-oneOfMembers}}
234234
{{#vendorExtensions.x-wrappedOneOfMembers}}
235-
case {{wrapperClassname}}(v) => Encoder[{{classname}}].apply(v).deepMerge(io.circe.Json.obj("{{discriminator.propertyName}}" -> "{{discriminatorValue}}".asJson))
235+
case {{wrapperClassname}}(v) => Encoder[{{classname}}].apply(v).deepMerge(io.circe.Json.obj("{{discriminator.propertyBaseName}}" -> "{{discriminatorValue}}".asJson))
236236
{{/vendorExtensions.x-wrappedOneOfMembers}}
237237
}
238238

239239
implicit val decoder: Decoder[{{classname}}] = Decoder.instance { c =>
240-
c.get[String]("{{discriminator.propertyName}}").flatMap {
240+
c.get[String]("{{discriminator.propertyBaseName}}").flatMap {
241241
{{#vendorExtensions.x-oneOfMembers}}
242242
case "{{vendorExtensions.x-discriminator-value}}" => c.as[{{classname}}]({{classname}}.decoder).map(x => x: {{parentClassname}})
243243
{{/vendorExtensions.x-oneOfMembers}}
244244
{{#vendorExtensions.x-wrappedOneOfMembers}}
245245
case "{{discriminatorValue}}" => c.as[{{classname}}]({{classname}}.decoder).map({{wrapperClassname}}.apply)
246246
{{/vendorExtensions.x-wrappedOneOfMembers}}
247-
case other => Left(io.circe.DecodingFailure(s"Unknown {{discriminator.propertyName}}: $$other", c.history))
247+
case other => Left(io.circe.DecodingFailure(s"Unknown {{discriminator.propertyBaseName}}: $$other", c.history))
248248
}
249249
}
250250
{{/vendorExtensions.x-use-discr}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/Sttp4CodegenTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ public void verifyOneOfSupportWithCirce() throws IOException {
113113
assertFileContains(vehiclePath, "\"Car\" => \"car\"");
114114
assertFileContains(vehiclePath, "\"Truck\" => \"truck\"");
115115

116+
// Test oneOf with discriminator that is a Scala keyword ("type")
117+
// The discriminator should use the original wire name, not the backtick-escaped Scala name
118+
Path shapePath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Shape.scala");
119+
assertFileContains(shapePath, "sealed trait Shape");
120+
assertFileContains(shapePath,
121+
"private implicit val config: Configuration = Configuration.default.withDiscriminator(\"type\")");
122+
// Discriminator in serialization must not be backtick-escaped
123+
assertFileNotContains(shapePath, "withDiscriminator(\"`type`\")");
124+
116125
// Verify regular models are still case classes
117126
Path dogPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Dog.scala");
118127
assertFileContains(dogPath, "case class Dog(");

modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,41 @@ components:
7272
car: '#/components/schemas/Car'
7373
truck: '#/components/schemas/Truck'
7474

75+
# OneOf with discriminator using a Scala keyword
76+
Shape:
77+
oneOf:
78+
- $ref: '#/components/schemas/Circle'
79+
- $ref: '#/components/schemas/Square'
80+
discriminator:
81+
propertyName: type
82+
mapping:
83+
circle: '#/components/schemas/Circle'
84+
square: '#/components/schemas/Square'
85+
86+
Circle:
87+
type: object
88+
required:
89+
- type
90+
- radius
91+
properties:
92+
type:
93+
type: string
94+
const: circle
95+
radius:
96+
type: number
97+
98+
Square:
99+
type: object
100+
required:
101+
- type
102+
- side
103+
properties:
104+
type:
105+
type: string
106+
const: square
107+
side:
108+
type: number
109+
75110
Car:
76111
type: object
77112
required:

0 commit comments

Comments
 (0)