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 an 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.client4.{ResponseAs, ResponseException, StringBody, asString}
import sttp.model.MediaType

import scala.xml.{NodeSeq, XML}

trait SttpScalaxbApi:
  case class XmlElementLabel(label: String)

  // request body
  def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = 
    val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope)
    StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml)

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

  // response body handling description
  def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String], B], Any] =
    asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeXml[B]))
      .showAs("either(as string, as xml)")

This would add asXml methods needed for serialization and 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: SyncBackend = DefaultSyncBackend()
// `Outer` and `Inner` classes are generated by scalaxb from xsd file
val requestPayload = Outer(Inner(42, b = true, "horses"), "cats") 

// imports sttp related serialization / deserialization logic
import sttpScalaxb.* 

// gives needed XmlElementLabel for the top XML node
given XmlElementLabel = XmlElementLabel("outer") 
// imports member of code generated by scalaxb, that provides `XMLFormat` for `Outer` type; 
// this import may differ depending on location of generated code
import generated.Generated_OuterFormat 

val response: Response[Either[ResponseException[String], Outer]] =
  basicRequest
    .post(uri"...")
    .body(asXml(requestPayload))
    .response(asXml[Outer])
    .send(backend)