Changes to multipart codec in > 0.20.x

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.

Actually, it works if import PartThing._ is in the Example. In your case, the import is in Codecs, while the compilation error happens elsewhere.

@ckipp01 is this still an issue? I would also suggest migrating to 1.x, 0.x versions are getting quite old :slight_smile:

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.

Not sure if things will work with that, but moving the import Codecs._ up makes the code compile:

object Example {
  import Data._
  import Schemas._
  import Codecs._

  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)
        )
      )
    )

  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")

}