sttp.tapir.Schema.scala Maven / Gradle / Ivy
package sttp.tapir
import sttp.model.{Part, Uri}
import sttp.tapir.Schema.{SName, Title}
import sttp.tapir.SchemaType._
import sttp.tapir.generic.{Configuration, Derived}
import sttp.tapir.internal.{ValidatorSyntax, isBasicValue}
import sttp.tapir.macros.{SchemaCompanionMacros, SchemaMacros}
import sttp.tapir.model.Delimited
import java.io.InputStream
import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger}
import java.nio.ByteBuffer
import java.time._
import java.util.{Date, UUID}
import scala.annotation.{StaticAnnotation, implicitNotFound}
/** Describes the type `T`: its low-level representation, meta-data and validation rules.
* @param format
* The name of the format of the low-level representation of `T`.
*/
@implicitNotFound(
msg = """Could not find Schema for type ${T}.
Schemas can be derived automatically by adding: `import sttp.tapir.generic.auto._`, or manually using `Schema.derived[T]`.
The latter is also useful for debugging derivation errors.
You can find more details in the docs: https://tapir.softwaremill.com/en/latest/endpoint/schemas.html
When integrating with a third-party datatype library remember to import respective schemas/codecs as described in https://tapir.softwaremill.com/en/latest/endpoint/integrations.html"""
)
case class Schema[T](
schemaType: SchemaType[T],
name: Option[SName] = None,
isOptional: Boolean = false,
description: Option[String] = None,
// The default value together with the value encoded to a raw format, which will then be directly rendered in the
// documentation. This is needed as codecs for nested types aren't available. Similar to Validator.EncodeToRaw
default: Option[(T, Option[Any])] = None,
format: Option[String] = None,
encodedExample: Option[Any] = None,
deprecated: Boolean = false,
hidden: Boolean = false,
validator: Validator[T] = Validator.pass[T],
attributes: AttributeMap = AttributeMap.Empty
) extends SchemaMacros[T] {
def map[TT](f: T => Option[TT])(g: TT => T): Schema[TT] = copy(
schemaType = schemaType.contramap(g),
default = default.flatMap { case (t, raw) =>
f(t).map(tt => (tt, raw))
},
validator = validator.contramap(g)
)
/** Adapt this schema to type `TT`. Only the meta-data is retained, except for default values and the validator (however, product
* field/subtypes validators are retained). Run-time functionality:
* - traversing collection elements, product fields, or coproduct subtypes
* - validating an instance of type `TT` the top-level type is lost.
*/
def as[TT]: Schema[TT] = copy(
schemaType = schemaType.as[TT],
default = None,
validator = Validator.pass
)
/** Returns an optional version of this schema, with `isOptional` set to true. */
def asOption: Schema[Option[T]] =
Schema(
schemaType = SOption(this)(identity),
isOptional = true,
format = format,
deprecated = deprecated,
hidden = hidden,
attributes = attributes
)
/** Returns an array version of this schema, with the schema type wrapped in [[SArray]]. Sets `isOptional` to true as the collection might
* be empty.
*/
def asArray: Schema[Array[T]] =
Schema(
schemaType = SArray(this)(_.toIterable),
isOptional = true,
deprecated = deprecated,
hidden = hidden,
attributes = attributes
)
/** Returns a collection version of this schema, with the schema type wrapped in [[SArray]]. Sets `isOptional` to true as the collection
* might be empty.
*/
def asIterable[C[X] <: Iterable[X]]: Schema[C[T]] =
Schema(
schemaType = SArray(this)(identity),
isOptional = true,
deprecated = deprecated,
hidden = hidden,
attributes = attributes
)
def name(name: SName): Schema[T] = copy(name = Some(name))
def name(name: Option[SName]): Schema[T] = copy(name = name)
def description(d: String): Schema[T] = copy(description = Some(d))
def encodedExample(e: Any): Schema[T] = copy(encodedExample = Some(e))
/** Adds a default value, which is used by [[Codec]] s during decoding and for documentation.
*
* To represent the value in the documentation, an encoded form needs to be provided. The encoded form is inferred if missing and the
* given value is of a basic type (number, string, etc.).
*/
def default(t: T, encoded: Option[Any] = None): Schema[T] = {
val raw2 = encoded match {
case None if isBasicValue(t) => Some(t)
case _ => encoded
}
copy(default = Some((t, raw2)), isOptional = true)
}
def format(f: String): Schema[T] = copy(format = Some(f))
def deprecated(d: Boolean): Schema[T] = copy(deprecated = d)
def hidden(h: Boolean): Schema[T] = copy(hidden = h)
def title(t: String): Schema[T] = attribute(Title.Attribute, Title(t))
def show: String = s"schema is $schemaType"
def showValidators: Option[String] = {
def showFieldValidators(fields: List[SProductField[T]]) = {
fields.map(f => f.schema.showValidators.map(fvs => s"${f.name.name}->($fvs)")).collect { case Some(s) => s } match {
case Nil => None
case l => Some(l.mkString(","))
}
}
if (hasValidation) {
val thisValidator = validator.show
val childValidators = schemaType match {
case SOption(element) => element.showValidators.map(esv => s"elements($esv)")
case SArray(element) => element.showValidators.map(esv => s"elements($esv)")
case SProduct(fields) => showFieldValidators(fields)
case SOpenProduct(fields, valueSchema) =>
val fieldValidators = showFieldValidators(fields)
val elementsValidators = valueSchema.showValidators.map(esv => s"elements($esv)")
(fieldValidators, elementsValidators) match {
case (None, None) => None
case (None, Some(_)) => elementsValidators
case (Some(_), None) => fieldValidators
case (Some(f), Some(e)) => Some(f + " " + e)
}
case SCoproduct(subtypes, _) =>
subtypes.map(s => s.showValidators.map(svs => s.name.fold(svs)(n => s"${n.show}->($svs)"))) match {
case Nil => None
case l => Some(l.mkString(","))
}
case SRef(_) => Some("recursive")
case _ => None
}
(thisValidator.toList ++ childValidators.toList) match {
case Nil => None
case l => Some(l.mkString(","))
}
} else None
}
/** See [[modify]]: instead of a path expressed using code, accepts a path a sequence of `String`s. */
def modifyUnsafe[U](fields: String*)(modify: Schema[U] => Schema[U]): Schema[T] = modifyAtPath(fields.toList, modify)
private def modifyAtPath[U](fieldPath: List[String], modify: Schema[U] => Schema[U]): Schema[T] =
fieldPath match {
case Nil => modify(this.asInstanceOf[Schema[U]]).asInstanceOf[Schema[T]] // we don't have type-polymorphic functions
case f :: fs =>
def modifyFieldsAtPath(fields: List[SProductField[T]]) = {
fields.map { field =>
if (field.name.name == f) SProductField[T, field.FieldType](field.name, field.schema.modifyAtPath(fs, modify), field.get)
else field
}
}
val schemaType2 = schemaType match {
case s @ SArray(element) if f == Schema.ModifyCollectionElements => SArray(element.modifyAtPath(fs, modify))(s.toIterable)
case s @ SOption(element) if f == Schema.ModifyCollectionElements => SOption(element.modifyAtPath(fs, modify))(s.toOption)
case s @ SProduct(fields) =>
s.copy(fields = modifyFieldsAtPath(fields))
case s @ SOpenProduct(fields, valueSchema) if f == Schema.ModifyCollectionElements =>
s.copy(
fields = modifyFieldsAtPath(fields),
valueSchema = valueSchema.modifyAtPath(fs, modify)
)(s.mapFieldValues)
case s @ SCoproduct(subtypes, _) =>
s.copy(subtypes = subtypes.map(_.modifyAtPath(fieldPath, modify)))(s.subtypeSchema)
case _ => schemaType
}
copy(schemaType = schemaType2)
}
/** Add a validator to this schema. If the validator contains a named enum validator:
* - the encode function is inferred if not yet defined, and the validators possible values are of a basic type
* - the name is set as the schema's name.
*/
def validate(v: Validator[T]): Schema[T] = {
val v2 = v.inferEnumerationEncode
// if there's an enum validator, propagating the name of the enumeration to the schema
v2.asPrimitiveValidators.collectFirst { case Validator.Enumeration(_, _, Some(name)) => name } match {
case Some(name) => copy(name = Some(name), validator = validator.and(v2))
case None => copy(validator = validator.and(v2))
}
}
/** Apply defined validation rules to the given value. */
def applyValidation(t: T): List[ValidationError[_]] = applyValidation(t, Map())
private def applyValidation(t: T, objects: Map[SName, Schema[_]]): List[ValidationError[_]] = {
val objects2 = name.fold(objects)(n => objects + (n -> this))
def applyFieldsValidation(fields: List[SProductField[T]]) = {
fields.flatMap(f => f.get(t).map(f.schema.applyValidation(_, objects2)).getOrElse(Nil).map(_.prependPath(f.name)))
}
// we avoid running validation for structures where there are no validation rules applied (recursively)
if (hasValidation) {
validator(t) ++ (schemaType match {
case s @ SOption(element) => s.toOption(t).toList.flatMap(element.applyValidation(_, objects2))
case s @ SArray(element) => s.toIterable(t).flatMap(element.applyValidation(_, objects2))
case s @ SProduct(_) => applyFieldsValidation(s.fieldsWithValidation)
case s @ SOpenProduct(_, valueSchema) =>
applyFieldsValidation(s.fieldsWithValidation) ++
s.mapFieldValues(t).flatMap { case (k, v) => valueSchema.applyValidation(v, objects2).map(_.prependPath(FieldName(k, k))) }
case s @ SCoproduct(_, _) =>
s.subtypeSchema(t)
.map { case SchemaWithValue(s, v) => s.applyValidation(v, objects2) }
.getOrElse(Nil)
case SRef(name) => objects.get(name).map(_.asInstanceOf[Schema[T]].applyValidation(t, objects2)).getOrElse(Nil)
case _ => Nil
})
} else Nil
}
private[tapir] def hasValidation: Boolean = {
(validator != Validator.pass) || (schemaType match {
case SOption(element) => element.hasValidation
case SArray(element) => element.hasValidation
case s: SProduct[T] => s.fieldsWithValidation.nonEmpty
case s @ SOpenProduct(_, valueSchema) => s.fieldsWithValidation.nonEmpty || valueSchema.hasValidation
case SCoproduct(subtypes, _) => subtypes.exists(_.hasValidation)
case SRef(_) => true
case _ => false
})
}
def attribute[A](k: AttributeKey[A]): Option[A] = attributes.get(k)
def attribute[A](k: AttributeKey[A], v: A): Schema[T] = copy(attributes = attributes.put(k, v))
}
object Schema extends LowPrioritySchema with SchemaCompanionMacros {
val ModifyCollectionElements = "each"
/** Creates a schema for type `T`, where the low-level representation is a `String`. */
def string[T]: Schema[T] = Schema(SString())
/** Creates a schema for type `T`, where the low-level representation is binary. */
def binary[T]: Schema[T] = Schema(SBinary())
implicit val schemaForString: Schema[String] = Schema(SString())
implicit val schemaForByte: Schema[Byte] = Schema(SInteger())
implicit val schemaForShort: Schema[Short] = Schema(SInteger())
implicit val schemaForInt: Schema[Int] = Schema(SInteger[Int]()).format("int32")
implicit val schemaForLong: Schema[Long] = Schema(SInteger[Long]()).format("int64")
implicit val schemaForFloat: Schema[Float] = Schema(SNumber[Float]()).format("float")
implicit val schemaForDouble: Schema[Double] = Schema(SNumber[Double]()).format("double")
implicit val schemaForBoolean: Schema[Boolean] = Schema(SBoolean())
implicit val schemaForUnit: Schema[Unit] = Schema(SProduct.empty)
implicit val schemaForFileRange: Schema[FileRange] = Schema(SBinary())
implicit val schemaForByteArray: Schema[Array[Byte]] = Schema(SBinary())
implicit val schemaForByteBuffer: Schema[ByteBuffer] = Schema(SBinary())
implicit val schemaForInputStream: Schema[InputStream] = Schema(SBinary())
implicit val schemaForInputStreamRange: Schema[InputStreamRange] = Schema(SchemaType.SBinary())
implicit val schemaForInstant: Schema[Instant] = Schema(SDateTime())
implicit val schemaForZonedDateTime: Schema[ZonedDateTime] = Schema(SDateTime())
implicit val schemaForOffsetDateTime: Schema[OffsetDateTime] = Schema(SDateTime())
implicit val schemaForDate: Schema[Date] = Schema(SDateTime())
implicit val schemaForLocalDateTime: Schema[LocalDateTime] = Schema(SString())
implicit val schemaForLocalDate: Schema[LocalDate] = Schema(SDate())
implicit val schemaForZoneOffset: Schema[ZoneOffset] = Schema(SString())
implicit val schemaForZoneId: Schema[ZoneId] = Schema(SString())
implicit val schemaForJavaDuration: Schema[Duration] = Schema(SString())
implicit val schemaForLocalTime: Schema[LocalTime] = Schema(SString())
implicit val schemaForOffsetTime: Schema[OffsetTime] = Schema(SString())
implicit val schemaForScalaDuration: Schema[scala.concurrent.duration.Duration] = Schema(SString())
implicit val schemaForUUID: Schema[UUID] = Schema(SString[UUID]()).format("uuid")
implicit val schemaForBigDecimal: Schema[BigDecimal] = Schema(SNumber())
implicit val schemaForJBigDecimal: Schema[JBigDecimal] = Schema(SNumber())
implicit val schemaForBigInt: Schema[BigInt] = Schema(SInteger())
implicit val schemaForJBigInteger: Schema[JBigInteger] = Schema(SInteger())
implicit val schemaForFile: Schema[TapirFile] = Schema(SBinary())
implicit val schemaForUri: Schema[Uri] = Schema(SString())
.encodedExample(Uri("https", "example.com"))
implicit def schemaForOption[T: Schema]: Schema[Option[T]] = implicitly[Schema[T]].asOption
implicit def schemaForArray[T: Schema]: Schema[Array[T]] = implicitly[Schema[T]].asArray
implicit def schemaForIterable[T: Schema, C[X] <: Iterable[X]]: Schema[C[T]] = implicitly[Schema[T]].asIterable[C]
implicit def schemaForSet[T: Schema, C[X] <: scala.collection.Set[X]]: Schema[C[T]] =
schemaForIterable[T, C].attribute(Schema.UniqueItems.Attribute, Schema.UniqueItems(true))
implicit def schemaForPart[T: Schema]: Schema[Part[T]] = implicitly[Schema[T]].map(_ => None)(_.body)
implicit def schemaForEither[A, B](implicit sa: Schema[A], sb: Schema[B]): Schema[Either[A, B]] = {
Schema[Either[A, B]](
SchemaType.SCoproduct(List(sa, sb), None) {
case Left(v) => Some(SchemaWithValue(sa, v))
case Right(v) => Some(SchemaWithValue(sb, v))
},
for {
na <- sa.name
nb <- sb.name
} yield Schema.SName("Either", List(na.show, nb.show))
)
}
implicit def schemaForDelimited[D <: String, T](implicit tSchema: Schema[T]): Schema[Delimited[D, T]] =
tSchema.asIterable[List].map(l => Some(Delimited[D, T](l)))(_.values).attribute(Explode.Attribute, Explode(false))
/** Corresponds to OpenAPI's `explode` parameter which should be used for delimited values. */
case class Explode(explode: Boolean)
object Explode {
val Attribute: AttributeKey[Explode] = new AttributeKey[Explode]("sttp.tapir.Schema.Explode")
}
/** Used in combination with explode, to properly represent delimited values in examples and default values (#3581) */
case class Delimiter(delimiter: String)
object Delimiter {
val Attribute: AttributeKey[Delimiter] = new AttributeKey[Delimiter]("sttp.tapir.Schema.Delimiter")
}
/** Corresponds to JsonSchema's `title` parameter which should be used for defining title of the object. */
case class Title(value: String)
object Title {
val Attribute: AttributeKey[Title] = new AttributeKey[Title]("sttp.tapir.Schema.Title")
}
case class UniqueItems(uniqueItems: Boolean)
object UniqueItems {
val Attribute: AttributeKey[UniqueItems] = new AttributeKey[UniqueItems]("sttp.tapir.Schema.UniqueItems")
}
/** @param typeParameterShortNames
* full name of type parameters, name is legacy and kept only for backward compatibility
*/
case class SName(fullName: String, typeParameterShortNames: List[String] = Nil) {
def show: String = fullName + (if (typeParameterShortNames.isEmpty) "" else typeParameterShortNames.mkString("[", ",", "]"))
}
object SName {
val Unit: SName = SName(fullName = "Unit")
}
/** Annotations which are used during automatic schema derivation, or semi-automatic schema derivation using [[Schema.derived]]. */
object annotations {
class description(val text: String) extends StaticAnnotation with Serializable
class encodedExample(val example: Any) extends StaticAnnotation with Serializable
class default[T](val default: T, val encoded: Option[Any] = None) extends StaticAnnotation with Serializable
class format(val format: String) extends StaticAnnotation with Serializable
class deprecated extends StaticAnnotation with Serializable
class hidden extends StaticAnnotation with Serializable
class encodedName(val name: String) extends StaticAnnotation with Serializable
class title(val name: String) extends StaticAnnotation with Serializable
/** Adds the `v` validator to the schema using [[Schema.validate]]. Note that the type of the validator must match exactly the type of
* the class/field. This is not checked at compile-time, and might cause run-time exceptions. To validate elements of collections or
* [[Option]]s, use [[validateEach]].
*/
class validate[T](val v: Validator[T]) extends StaticAnnotation with Serializable
/** Adds the `v` validators to elements of the schema, when the annotated class or field is a collection or [[Option]]. The type of the
* validator must match exactly the type of the collection's elements. This is not checked at compile-time, and might cause run-time
* exceptions. E.g. to validate that when an `Option[Int]` is defined, the value is smaller than 5, you should use:
* {{{
* case class Payload(
* @validateEach(Validator.max(4, exclusive = true))
* aField: Option[Int]
* )
* }}}
*/
class validateEach[T](val v: Validator[T]) extends StaticAnnotation with Serializable
class customise(val f: Schema[_] => Schema[_]) extends StaticAnnotation with Serializable
}
/** Wraps the given schema with a single-field product, where `fieldName` maps to `schema`.
*
* The resulting schema has no name.
*
* Useful when generating one-of schemas for coproducts, where to discriminate between child types a wrapper product is used. To
* automatically derive such a schema for a sealed hierarchy, see [[Schema.oneOfWrapped]].
*/
def wrapWithSingleFieldProduct[T](schema: Schema[T], fieldName: FieldName): Schema[T] = Schema(
schemaType = SchemaType.SProduct(List(SchemaType.SProductField[T, T](fieldName, schema, t => Some(t))))
)
/** Wraps the given schema with a single-field product, where a field computed using the implicit [[Configuration]] maps to `schema`.
*
* The resulting schema has no name.
*
* Useful when generating one-of schemas for coproducts, where to discriminate between child types a wrapper product is used. To
* automatically derive such a schema for a sealed hierarchy, see [[Schema.oneOfWrapped]].
*/
def wrapWithSingleFieldProduct[T](schema: Schema[T])(implicit conf: Configuration): Schema[T] =
wrapWithSingleFieldProduct(schema, FieldName(conf.toDiscriminatorValue(schema.name.getOrElse(SName.Unit))))
/** A schema allowing anything: a number, string, object, etc. A [[SCoproduct]] with no specified subtypes.
* @see
* [[anyObject]]
*/
def any[T]: Schema[T] = Schema(SCoproduct(Nil, None)(_ => None), None)
/** A schema allowing any object. A [[SProduct]] with no specified fields.
* @see
* [[any]]
*/
def anyObject[T]: Schema[T] = Schema(SProduct(Nil), None)
}
trait LowPrioritySchema {
implicit def derivedSchema[T](implicit derived: Derived[Schema[T]]): Schema[T] = derived.value
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy