I have some example code that I’m a bit confused with as it’s working in 0.19.x, but I’m pretty sure breaks as of this commit. In the changelog for the 0.20.x series I noticed that the breaking changes mentioned
optional parts in a multipart codec are now represented as a Option[Part[T]], instead of an Part[Option[T]]
I originally thought this wouldn’t affect our app, but I do hit on issues that I don’t fully understand. I have a code snippet illustrating the issue we are hitting on (it’s odd, but minimized to show how we’re using it exactly).
When using 0.19.4 and everything works
//> using scala 2.13
//> using lib com.softwaremill.sttp.tapir::tapir-core:0.19.4
import sttp.tapir.CodecFormat
import sttp.tapir._
import sttp.model.Part
import sttp.tapir.Schema
import sttp.tapir.Codec.XmlCodec
object Data {
final case class Thing(a: Int, b: Int)
final case class PartThing(module: Part[Thing])
object PartThing {
implicit lazy val sOther: Schema[Thing] =
Schema.derived[Thing]
implicit lazy val sExampleUpload: Schema[PartThing] =
Schema.derived[PartThing]
}
}
object Codecs {
import Data._
import PartThing._
implicit lazy val xmlDefinition: XmlCodec[Thing] =
Codec.xml { _ =>
DecodeResult.Value(Thing(1, 2))
}(_ => "")
implicit lazy val xmlFileCodec: Codec[List[String], Thing, CodecFormat.Xml] =
Codec.listHead(xmlDefinition)
}
object Example {
import Data._
def fileUpload[C <: CodecFormat](
format: C
)(implicit
codec: Codec[List[String], Thing, C]
): EndpointIO.Body[Seq[RawPart], Thing] =
multipartBody[PartThing].map(_.module.body)(module =>
PartThing(
Part(
name = "",
body = module,
contentType = Some(format.mediaType)
)
)
)
import Codecs._
val example: Endpoint[Unit, Thing, Unit, Thing, Any] =
endpoint
.tag("examples")
.post
.in("example")
.in(fileUpload(CodecFormat.Xml()))
.out(fileUpload(CodecFormat.Xml()))
.name("echo files")
}
If you then update the lib to anything above 0.20.x
- //> using lib com.softwaremill.sttp.tapir::tapir-core:0.19.4
+ //> using lib com.softwaremill.sttp.tapir::tapir-core:0.20.2
This will fail with:
Cannot find a codec between a List[Part[T]] for some basic type T and: value module
Could someone explain what is going on here? I thought maybe the implicit codec being passed in also needed to be wrapped in a Part, but that doesn’t seem to fix this scenario.
Could you move the implicit Schema[Thing] to a companion object of Thing? This resolves the issue, although I’m not sure why import PartThing._ doesn’t work.
Ah, nice yes, this does fix it. The tricky part is the reason I have the example like this is because Thing in our application is actually a generated type, so we don’t really have a way to move the Schema[Thing] into the companion object.
Hey! Sorry, I got pulled into something else, but I’m back! Yes, we’re still hitting on this.
I would also suggest migrating to 1.x, 0.x versions are getting quite old
Ha, yes, that’s the plan. We are updating to 1.x, but I just noticed that the actual issue we were stuck on got introduced in anything over 0.20.x, so that’s why I included that info. As I mentioned before the tricky part is that some of our types are generated, so we don’t have access to drop the schema in the companion object. I’ve mimicked this just including an external type. You can see a full code example below:
//> using scala 2.13
//> using lib com.softwaremill.sttp.tapir::tapir-core:1.9.1
//> using lib org.typelevel::cats-core::2.10.0
import sttp.tapir.CodecFormat
import sttp.tapir._
import sttp.model.Part
import sttp.tapir.Schema
import sttp.tapir.Codec.XmlCodec
import cats.data.NonEmptyList
object Schemas {
implicit lazy val nonEmptyListSchema: Schema[NonEmptyList[Int]] =
Schema.derived[NonEmptyList[Int]]
}
object Data {
final case class PartThing(module: Part[NonEmptyList[Int]])
object PartThing {
import Schemas._
implicit lazy val sExampleUpload: Schema[PartThing] =
Schema.derived[PartThing]
}
}
object Codecs {
import Data._
import PartThing._
import Schemas._
implicit lazy val xmlDefinition: XmlCodec[NonEmptyList[Int]] =
Codec.xml { _ =>
DecodeResult.Value(NonEmptyList(1, Nil))
}(_ => "")
implicit lazy val xmlFileCodec
: Codec[List[String], NonEmptyList[Int], CodecFormat.Xml] =
Codec.listHead(xmlDefinition)
}
object Example {
import Data._
import Schemas._
def fileUpload[C <: CodecFormat](
format: C
)(implicit
codec: Codec[List[String], NonEmptyList[Int], C]
): EndpointIO.Body[Seq[RawPart], NonEmptyList[Int]] =
multipartBody[PartThing].map(_.module.body)(module =>
PartThing(
Part(
name = "",
body = module,
contentType = Some(format.mediaType)
)
)
)
import Codecs._
val example: Endpoint[Unit, NonEmptyList[Int], Unit, NonEmptyList[Int], Any] =
endpoint
.tag("examples")
.post
.in("example")
.in(fileUpload(CodecFormat.Xml()))
.out(fileUpload(CodecFormat.Xml()))
.name("echo files")
}
The import of Schemes._ works in the top two places needed, but not where multipartBody is used and it’s also needed. What I don’t fully understand is why it can’t find it here. Any insight would be greatly appreciated.