Working with XML
Enabling support for XML is a matter of implementing proper XmlCodec[T]
and providing it in scope.
This enables encoding objects to XML strings, and decoding XML strings to objects.
Implementation is fairly easy, and for now, one guide on how to integrate with scalaxb is provided.
Note
Note, that implementing XmlCodec[T]
would require deriving not only XML library encoders/decoders,
but also tapir related Schema[T]
. These are completely separate - any customization e.g. for field
naming or inheritance strategies must be done separately for both derivations.
For more details see sections on schema derivation
and on supporting custom types in general.
Scalaxb
If you possess the XML Schema definition file (.xsd
file) consider using the scalaxb tool,
which generates needed models and serialization/deserialization logic.
To use the tool please follow the documentation on setting up and
running scalaxb.
After code generation, create the TapirXmlScalaxb
trait (or trait with another name of your choosing) and add the following code snippet:
import generated.`package`.defaultScope // import may differ depending on location of generated code
import scalaxb.XMLFormat // import may differ depending on location of generated code
import scalaxb.`package`.{fromXML, toXML} // import may differ depending on location of generated code
import sttp.tapir.Codec.XmlCodec
import sttp.tapir.DecodeResult.{Error, Value}
import sttp.tapir.{Codec, EndpointIO, Schema, stringBodyUtf8AnyFormat}
import scala.xml.{NodeSeq, XML}
trait TapirXmlScalaxb {
case class XmlElementLabel(label: String)
def xmlBody[T: XMLFormat: Schema](implicit l: XmlElementLabel): EndpointIO.Body[String, T] = stringBodyUtf8AnyFormat(scalaxbCodec[T])
implicit def scalaxbCodec[T: XMLFormat: Schema](implicit label: XmlElementLabel): XmlCodec[T] = {
Codec.xml((s: String) =>
try {
Value(fromXML[T](XML.loadString(s)))
} catch {
case e: Exception => Error(s, e)
}
)((t: T) => {
val nodeSeq: NodeSeq = toXML[T](obj = t, elementLabel = label.label, scope = defaultScope)
nodeSeq.toString()
})
}
}
This creates XmlCodec[T]
that would encode / decode the types with XMLFormat
, Schema
and with XmlElementLabel
provided in scope.
It also introduces xmlBody
helper method, which allows you to easily express, that the declared endpoint consumes or returns XML.
Next to this trait, you might want to introduce xml
package object to simplify imports.
package object xml extends TapirXmlScalaxb
From now on, XML serialization/deserialization would work for all classes generated from .xsd
file as long as
XMLFormat
, Schema
and XmlElementLabel
would be implicitly provided in the scope.
XMLFormat
is scalaxb related, allowing for XML encoding / decoding.
Schema
is tapir related, used primarily when generating documentation and validating incoming values.
And XmlElementLabel
is required by scalaxb code when encoding to XML to give proper top node name.
Usage example:
import sttp.tapir.{PublicEndpoint, endpoint}
import cats.effect.IO
import generated.Outer // import may differ depending on location of generated code
import sttp.tapir.generic.auto._ // needed for Schema derivation
import sttp.tapir.server.ServerEndpoint
object Endpoints {
import xml._ // imports tapir related serialization / deserialization logic
implicit val label: XmlElementLabel = XmlElementLabel("outer") // `label` is needed by scalaxb code to properly encode the top node of the xml
val xmlEndpoint: PublicEndpoint[Outer, Unit, Outer, Any] = endpoint.post // `Outer` is a class generated by scalaxb based on .xsd file.
.in("xml")
.in(xmlBody[Outer])
.out(xmlBody[Outer])
}
If the generation of OpenAPI documentation is required, consider adding OpenAPI doc extension on schema providing XML
namespace as described in the “Prefixes and Namespaces” section at OpenAPI documentation regarding handling XML.
This would add xmlns
property to example request/responses at swagger, which is required by scalaxb to properly deserialize XML.
For more information on adding OpenAPI doc extension in tapir refer to documentation.
Adding xml namespace doc extension to Outer
’s Schema
example:
case class XmlNamespace(namespace: String)
implicit val outerSchemaWithXmlNamespace: Schema[Outer] = implicitly[Derived[Schema[Outer]]].value
.docsExtension("xml", XmlNamespace("http://www.example.com/innerouter"))
Also, you might want to check the repository with example project showcasing integration with tapir and scalaxb.