XML
Adding XML encoding/decoding support is a matter of providing a body serializer and/or a response body specification (similarly as it is done for JSON format). The process of adding integrations is fairly easy, and for now, one guide on how to use scalaxb tool is provided.
scalaxb
If you possess the XML Schema definition file (.xsd
file) consider using the scalaxb tool, which would generate 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 SttpScalaxbApi
trait (or trait with another name of your choosing) and add the following code snippet:
import generated.defaultScope // import may differ depending on location of generated code
import scalaxb.`package`.{fromXML, toXML} // import may differ depending on location of generated code
import scalaxb.{CanWriteXML, XMLFormat} // import may differ depending on location of generated code
import sttp.client3.{BodySerializer, ResponseAs, ResponseException, StringBody, asString}
import sttp.model.MediaType
import scala.xml.{NodeSeq, XML}
trait SttpScalaxbApi {
case class XmlElementLabel(label: String)
implicit def scalaxbBodySerializer[B](implicit format: CanWriteXML[B], label: XmlElementLabel): BodySerializer[B] = { (b: B) =>
val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope)
StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml)
}
implicit def deserializeXml[B](implicit decoder: XMLFormat[B]): String => Either[Exception, B] = { (s: String) =>
try {
Right(fromXML[B](XML.loadString(s)))
} catch {
case e: Exception => Left(e)
}
}
def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String, Exception], B], Any] =
asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeXml[B]))
.showAs("either(as string, as xml)")
}
This would add BodySerializer
needed for serialization and asXml
method needed for deserialization. Please notice, that fromXML
, toXML
, CanWriteXML
, XMLFormat
and defaultScope
are members of code generated by scalaxb.
Next to this trait, you might want to introduce sttpScalaxb
package object to simplify imports.
package object sttpScalaxb extends SttpScalaxbApi
From now on, XML serialization/deserialization would work for all classes generated from .xsd
file as long as XMLFormat
for the type in the question and XmlElementLabel
for the top XML node would be implicitly provided in the scope.
Usage example:
val backend: SttpBackend[Identity, Any] = HttpClientSyncBackend()
val requestPayload = Outer(Inner(42, b = true, "horses"), "cats") // `Outer` and `Inner` classes are generated by scalaxb from xsd file
import sttpScalaxb._ // imports sttp related serialization / deserialization logic
implicit val label = XmlElementLabel("outer") // gives needed XmlElementLabel for the top XML node
import generated.Generated_OuterFormat // imports member of code generated by scalaxb, that provides `XMLFormat` for `Outer` type; this import may differ depending on location of generated code
val response: Identity[Response[Either[ResponseException[String, Exception], Outer]]] =
basicRequest
.post(uri"...")
.body(requestPayload)
.response(asXml[Outer])
.send(backend)