Skip to content

Commit 33dfeba

Browse files
authored
Properly derive schemas for tuples (#3954)
1 parent 5c3c60d commit 33dfeba

5 files changed

Lines changed: 18 additions & 13 deletions

File tree

core/src/main/scala-2/sttp/tapir/generic/auto/SchemaMagnoliaDerivation.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ trait SchemaMagnoliaDerivation {
1515

1616
def join[T](ctx: ReadOnlyCaseClass[Schema, T])(implicit genericDerivationConfig: Configuration): Schema[T] = {
1717
withCache(ctx.typeName, ctx.annotations) {
18-
val result =
18+
var result =
1919
if (ctx.isValueClass) {
2020
require(ctx.parameters.nonEmpty, s"Cannot derive schema for generic value class: ${ctx.typeName.owner}")
2121
val valueSchema = ctx.parameters.head.typeclass
@@ -24,6 +24,9 @@ trait SchemaMagnoliaDerivation {
2424
// Not using inherited annotations when generating type name, we don't want @encodedName to be inherited for types
2525
Schema[T](schemaType = productSchemaType(ctx), name = Some(typeNameToSchemaName(ctx.typeName, ctx.annotations)))
2626
}
27+
if (ctx.typeName.full.startsWith("scala.Tuple")) {
28+
result = result.attribute(Schema.Tuple.Attribute, Schema.Tuple(true))
29+
}
2730
enrichSchema(result, mergeAnnotations(ctx.annotations, ctx.inheritedAnnotations))
2831
}
2932
}

core/src/main/scala-3/sttp/tapir/generic/auto/SchemaMagnoliaDerivation.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ trait SchemaMagnoliaDerivation {
1818

1919
override def join[T](ctx: CaseClass[Schema, T]): Schema[T] = {
2020
withCache(ctx.typeInfo, ctx.annotations) {
21-
val result =
21+
var result =
2222
if (ctx.isValueClass) {
2323
require(ctx.params.nonEmpty, s"Cannot derive schema for generic value class: ${ctx.typeInfo.owner}")
2424
val valueSchema = ctx.params.head.typeclass
@@ -27,6 +27,9 @@ trait SchemaMagnoliaDerivation {
2727
// Not using inherited annotations when generating type name, we don't want @encodedName to be inherited for types
2828
Schema[T](schemaType = productSchemaType(ctx), name = Some(typeNameToSchemaName(ctx.typeInfo, ctx.annotations)))
2929
}
30+
if (ctx.typeInfo.full.startsWith("scala.Tuple")) {
31+
result = result.attribute(Schema.Tuple.Attribute, Schema.Tuple(true))
32+
}
3033
enrichSchema(result, mergeAnnotations(ctx.annotations, ctx.inheritedAnnotations))
3134
}
3235
}

core/src/main/scala/sttp/tapir/Schema.scala

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -350,15 +350,15 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
350350
val Attribute: AttributeKey[UniqueItems] = new AttributeKey[UniqueItems]("sttp.tapir.Schema.UniqueItems")
351351
}
352352

353-
/** Hints that a [[SchemaType.SProduct]] should be rendered in the schema as an `array` (#3941).
353+
/** Specifies that the given schema is for a tuple. Tuples are products with no meaningful property names - attributes are identified by
354+
* their position.
354355
*
355-
* Used to model tuples, which are products with no meaningful property names - attributes are identified by their position. When
356-
* converting to JSON schema, adding this attribute on a schema for a `SProduct` renders `array` schema, with type constraints for each
357-
* index.
356+
* When converting a tuple schema of type [[SchemaType.SProduct]] to JSON schema, renders as an `array` schema, with type constraints for
357+
* each index (#3941).
358358
*/
359-
case class ProductAsArray(productAsArray: Boolean)
360-
object ProductAsArray {
361-
val Attribute: AttributeKey[ProductAsArray] = new AttributeKey[ProductAsArray]("sttp.tapir.Schema.ProductAsArray")
359+
case class Tuple(isTuple: Boolean)
360+
object Tuple {
361+
val Attribute: AttributeKey[Tuple] = new AttributeKey[Tuple]("sttp.tapir.Schema.Tuple")
362362
}
363363

364364
/** @param typeParameterShortNames

docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/TSchemaToASchema.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ private[docs] class TSchemaToASchema(
3333
case TSchemaType.SNumber() => ASchema(SchemaType.Number)
3434
case TSchemaType.SBoolean() => ASchema(SchemaType.Boolean)
3535
case TSchemaType.SString() => ASchema(SchemaType.String)
36-
case TSchemaType.SProduct(fields) if schema.attribute(TSchema.ProductAsArray.Attribute).map(_.productAsArray).getOrElse(false) =>
36+
case TSchemaType.SProduct(fields) if schema.attribute(TSchema.Tuple.Attribute).map(_.isTuple).getOrElse(false) =>
3737
ASchema(SchemaType.Array).copy(
3838
prefixItems = Some(fields.map(f => apply(f.schema, allowReference = true)))
3939
)

docs/apispec-docs/src/test/scala/sttp/tapir/docs/apispec/schema/TapirSchemaToJsonSchemaTest.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import sttp.tapir.Schema.annotations.title
1313
import sttp.tapir._
1414
import sttp.tapir.generic.auto._
1515

16-
class JsonSchemasTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues with Inside {
16+
class TapirSchemaToJsonSchemaTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues with Inside {
1717
behavior of "TapirSchemaToJsonSchema"
1818

1919
it should "represent schema as JSON" in {
@@ -150,9 +150,8 @@ class JsonSchemasTest extends AnyFlatSpec with Matchers with OptionValues with E
150150
TapirSchemaToJsonSchema(schema2, true).title shouldBe Some("Map_Either_Int_Node_Int_String")
151151
}
152152

153-
it should "Generate array for products marked with ProductAsArray attribute" in {
153+
it should "Generate array for products marked with Tuple attribute" in {
154154
val tSchema: Schema[(Int, String)] = implicitly[Schema[(Int, String)]]
155-
.attribute(Schema.ProductAsArray.Attribute, Schema.ProductAsArray(true))
156155

157156
// when
158157
val result = TapirSchemaToJsonSchema(tSchema, markOptionsAsNullable = false)

0 commit comments

Comments
 (0)