Working with JSON

Json values are supported through codecs, which encode/decode values to json strings. Most often, you’ll be using a third-party library to perform the actual json parsing/printing. See below for the list of supported libraries.

All the integrations, when imported into scope, define jsonBody[T] and jsonQuery[T] methods.

Instead of providing the json codec as an implicit value, this method depends on library-specific implicits being in scope, and basing on these values creates a json codec. The derivation also requires an implicit Schema[T] instance, which can be automatically derived. For more details see sections on schema derivation and on supporting custom types in general. Such a design provides better error reporting, in case one of the components required to create the json codec is missing.

Note

Note that the process of deriving schemas, and deriving library-specific json encoders and decoders is entirely separate. The first is controlled by tapir, the second - by the json library. Any customisation, e.g. for field naming or inheritance strategies, must be done separately for both derivations.

Implicit json codecs

If you have a custom, implicit Codec[String, T, Json] instance, you should use the customJsonBody[T] method instead. This description of endpoint input/output, instead of deriving a codec basing on other library-specific implicits, uses the json codec that is in scope.

JSON as string

If you’d like to work with JSON bodies in a serialised String form, instead of integrating on a higher level using one of the libraries mentioned below, you should use the stringJsonBody input/output. Note that in this case, the serialising/deserialising of the body must be part of the server logic.

A schema can be provided in this case as well:

import sttp.tapir._
import sttp.tapir.generic.auto._
case class MyBody(field: Int)
stringJsonBody.schema(implicitly[Schema[MyBody]].as[String])

Circe

To use Circe, add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.2.9"

Next, import the package (or extend the TapirJsonCirce trait, see MyTapir):

import sttp.tapir.json.circe._

The above import brings into scope the jsonBody[T] body input/output description, which creates a codec, given an in-scope circe Encoder/Decoder and a Schema. Circe includes a couple of approaches to generating encoders/decoders (manual, semi-auto and auto), so you may choose whatever suits you.

Note that when using Circe’s auto derivation, any encoders/decoders for custom types must be in scope as well.

For example, to automatically generate a JSON codec for a case class:

import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
import io.circe.generic.auto._

case class Book(author: String, title: String, year: Int)

val bookInput: EndpointIO[Book] = jsonBody[Book]

Configuring the circe printer

Circe lets you select an instance of io.circe.Printer to configure the way JSON objects are rendered. By default Tapir uses Printer.nospaces, which would render:

import io.circe._

Json.obj(
  "key1" -> Json.fromString("present"),
  "key2" -> Json.Null
)

as

{"key1":"present","key2":null}

Suppose we would instead want to omit null-values from the object and pretty-print it. You can configure this by overriding the jsonPrinter in tapir.circe.json.TapirJsonCirce:

import sttp.tapir.json.circe._
import io.circe.Printer

object MyTapirJsonCirce extends TapirJsonCirce {
  override def jsonPrinter: Printer = Printer.spaces2.copy(dropNullValues = true)
}

import MyTapirJsonCirce._

Now the above JSON object will render as

{"key1":"present"}

µPickle

To use µPickle add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-upickle" % "1.2.9"

Next, import the package (or extend the TapirJsonuPickle trait, see MyTapir and add TapirJsonuPickle not TapirCirceJson):

import sttp.tapir.json.upickle._

µPickle requires a ReadWriter in scope for each type you want to serialize. In order to provide one use the macroRW macro in the companion object as follows:

import sttp.tapir._
import sttp.tapir.generic.auto._
import upickle.default._
import sttp.tapir.json.upickle._

case class Book(author: String, title: String, year: Int)

object Book {
  implicit val rw: ReadWriter[Book] = macroRW
}

val bookInput: EndpointIO[Book] = jsonBody[Book]

Like Circe, µPickle allows you to control the rendered json output. Please see the Custom Configuration of the manual for details.

For more examples, including making a custom encoder/decoder, see TapirJsonuPickleTests.scala

Play JSON

To use Play JSON add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-play" % "1.2.9"

Next, import the package (or extend the TapirJsonPlay trait, see MyTapir and add TapirJsonPlay not TapirCirceJson):

import sttp.tapir.json.play._

Play JSON requires Reads and Writes implicit values in scope for each type you want to serialize.

Spray JSON

To use Spray JSON add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-spray" % "1.2.9"

Next, import the package (or extend the TapirJsonSpray trait, see MyTapir and add TapirJsonSpray not TapirCirceJson):

import sttp.tapir.json.spray._

Spray JSON requires a JsonFormat implicit value in scope for each type you want to serialize.

Tethys JSON

To use Tethys JSON add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-tethys" % "1.2.9"

Next, import the package (or extend the TapirJsonTethys trait, see MyTapir and add TapirJsonTethys not TapirCirceJson):

import sttp.tapir.json.tethysjson._

Tethys JSON requires JsonReader and JsonWriter implicit values in scope for each type you want to serialize.

Jsoniter Scala

To use Jsoniter-scala add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-jsoniter-scala" % "1.2.9"

Next, import the package (or extend the TapirJsonJsoniter trait, see MyTapir and add TapirJsonJsoniter not TapirCirceJson):

import sttp.tapir.json.jsoniter._

Jsoniter Scala requires JsonValueCodec implicit value in scope for each type you want to serialize.

Json4s

To use json4s add the following dependencies to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-json4s" % "1.2.9"

And one of the implementations:

"org.json4s" %% "json4s-native" % "4.0.6"
// Or
"org.json4s" %% "json4s-jackson" % "4.0.6"

Next, import the package (or extend the TapirJson4s trait, see MyTapir and add TapirJson4s instead of TapirCirceJson):

import sttp.tapir.json.json4s._

Json4s requires Serialization and Formats implicit values in scope, for example:

import org.json4s._
// ...
implicit val serialization: Serialization = org.json4s.jackson.Serialization
implicit val formats: Formats = org.json4s.jackson.Serialization.formats(NoTypeHints)

Zio JSON

To use zio-json, add the following dependency to your project:

"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % "1.2.9"

Next, import the package (or extend the TapirJsonZio trait, see MyTapir and add TapirJsonZio instead of TapirCirceJson):

import sttp.tapir.json.zio._

Zio JSON requires JsonEncoder and JsonDecoder implicit values in scope for each type you want to serialize.

JSON query parameters

You can specify query parameters in JSON format by using the jsonQuery method. For example, using Circe:

import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
import io.circe.generic.auto._

case class Book(author: String, title: String, year: Int)

val bookQuery: EndpointInput.Query[Book] = jsonQuery[Book]("book")

Other JSON libraries

To add support for additional JSON libraries, see the sources for the Circe codec (which is just a couple of lines of code).

Coproducts (enums, sealed traits, classes)

If you are serialising a sealed hierarchy, such as a Scala 3 enum, a sealed trait or sealed class, the configuration of schema derivation will have to match the configuration of your json library. Different json libraries have different defaults when it comes to a discrimination strategy, so in order to have the schemas (and hence the documentation) in sync with how the values are serialised, you will have to configure schema derivation as well.

Schemas are referenced at the point of jsonBody and jsonQuery usage, so any configuration must be available in the implicit scope when these methods are called.

Next

Read on about working with forms.